Code Sketch


yoi
By: Mhalsakant School
Category: Programming
import java.awt.BasicStroke
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Cursor
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.Font
import java.awt.GradientPaint
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Toolkit
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.nio.file.Files
import java.awt.RenderingHints
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.MouseMotionAdapter
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.datatransfer.DataFlavor
import java.awt.geom.Path2D
import java.util.Random

import javax.swing.DefaultListModel
import javax.swing.JButton
import javax.swing.JColorChooser
import javax.swing.ImageIcon
import javax.swing.filechooser.FileNameExtensionFilter
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.JScrollPane
import javax.swing.JSplitPane
import javax.swing.JTabbedPane
import javax.swing.JTextArea
import javax.swing.JTextField
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.SwingUtilities
import javax.swing.Timer
import javax.swing.WindowConstants
import javax.swing.TransferHandler

import scala.collection.mutable.ArrayBuffer

// ============================================================
// ULTRA LEGEND X9
// DRAW + OBJECT + SHAPE + ANIMATION + GAMES STUDIO
// FULL STABLE VERSION
// ============================================================

// ============================================================
// CONSTANTS
// ============================================================

val CANVAS_W: Int = 1000
val CANVAS_H: Int = 650

val CENTER_X: Int = CANVAS_W / 2
val CENTER_Y: Int = CANVAS_H / 2

val RANDOM_GEN =
  new Random()

// ============================================================
// DATA MODELS
// ============================================================

case class DrawItem(
  points: Vector[Point],
  stroke: Color,
  fill: Color,
  filled: Boolean,
  name: String,
  visible: Boolean,
  locked: Boolean
)

case class PaddleLine(
  p1: Point,
  p2: Point
)

case class GameInfo(
  name: String,
  mode: String,
  description: String
)

// ============================================================
// GLOBAL DRAW STATE
// ============================================================

var drawItems =
  ArrayBuffer[DrawItem]()

var undoStack =
  ArrayBuffer[ArrayBuffer[DrawItem]]()

var redoStack =
  ArrayBuffer[ArrayBuffer[DrawItem]]()

var activeTool: String =
  "PENCIL"

var strokeColor: Color =
  new Color(25, 90, 220)

var fillColor: Color =
  new Color(255, 180, 40)

var brushWidth: Float =
  5.0f

var showGrid: Boolean =
  true

var snapToGrid: Boolean =
  false

var selectedIndex: Int =
  -1

var drawingNow: Boolean =
  false

var movingNow: Boolean =
  false

var resizingNow: Boolean =
  false

var resizeHandle: String =
  ""

var animationFrames =
  ArrayBuffer[ArrayBuffer[DrawItem]]()

var animationClipboard =
  ArrayBuffer[DrawItem]()

var animationCurrentFrame: Int =
  0

var animationFps: Int =
  8

var animationStudioOpen: Boolean =
  false

var lastMousePoint: Point =
  new Point(0, 0)

var originalPoints: Vector[Point] =
  Vector.empty

var tempPoints =
  ArrayBuffer[Point]()

var generatedCode: String =
  ""

var generatedAnimationCode: String =
  ""

// ============================================================
// IMPORTED MEDIA STATE
// ============================================================

var importedImage: BufferedImage =
  null

var importedImageOriginal: BufferedImage =
  null

var importedImagePath: String =
  ""

var importedImageName: String =
  ""

var importedVideoPath: String =
  ""

var importedVideoName: String =
  ""

var mainFrame: JFrame =
  null

var canvas: JPanel =
  null

var codeArea: JTextArea =
  null

var statusLabel: JLabel =
  null

var objectSearch: JTextField =
  null

var objectList: JList[String] =
  null

var shapeList: JList[String] =
  null

// ============================================================
// UI HELPERS
// ============================================================

def button(text: String): JButton = {
  val b =
    new JButton(text)

  b.setFocusable(false)

  b.setFont(
    new Font(
      "Arial",
      Font.BOLD,
      11
    )
  )

  b
}

def setStatus(text: String): Unit = {
  if (statusLabel != null) {
    statusLabel.setText(text)
  }
}

def repaintCanvas(): Unit = {
  if (canvas != null) {
    canvas.repaint()
  }
}

// ============================================================
// HISTORY
// ============================================================

def cloneItems(
  source: ArrayBuffer[DrawItem]
): ArrayBuffer[DrawItem] = {

  val output =
    ArrayBuffer[DrawItem]()

  var i: Int = 0

  while (i < source.length) {
    val item =
      source(i)

    val pts =
      item.points.map { p =>
        new Point(
          p.x,
          p.y
        )
      }

    output +=
      item.copy(
        points = pts
      )

    i += 1
  }

  output
}

def saveHistory(): Unit = {
  undoStack +=
    cloneItems(drawItems)

  redoStack.clear()

  while (undoStack.length > 60) {
    undoStack.remove(0)
  }
}

def undo(): Unit = {

  if (undoStack.nonEmpty) {

    redoStack +=
      cloneItems(drawItems)

    val previous =
      undoStack.last

    undoStack.remove(
      undoStack.length - 1
    )

    drawItems.clear()

    drawItems ++=
      cloneItems(previous)

    selectedIndex =
      -1

    repaintCanvas()

    setStatus(
      "UNDO COMPLETE"
    )

  } else {
    setStatus(
      "NOTHING TO UNDO"
    )
  }
}

def redo(): Unit = {

  if (redoStack.nonEmpty) {

    undoStack +=
      cloneItems(drawItems)

    val nextState =
      redoStack.last

    redoStack.remove(
      redoStack.length - 1
    )

    drawItems.clear()

    drawItems ++=
      cloneItems(nextState)

    selectedIndex =
      -1

    repaintCanvas()

    setStatus(
      "REDO COMPLETE"
    )

  } else {
    setStatus(
      "NOTHING TO REDO"
    )
  }
}

// ============================================================
// POINT HELPERS
// ============================================================

def distance(
  a: Point,
  b: Point
): Double = {
  math.hypot(
    b.x - a.x,
    b.y - a.y
  )
}

def snapped(value: Int): Int = {
  if (snapToGrid) {
    (value / 10) * 10
  } else {
    value
  }
}

def canvasPoint(
  e: MouseEvent
): Point = {

  val x =
    math.max(
      0,
      math.min(
        CANVAS_W - 1,
        snapped(e.getX)
      )
    )

  val y =
    math.max(
      0,
      math.min(
        CANVAS_H - 1,
        snapped(e.getY)
      )
    )

  new Point(
    x,
    y
  )
}

def closed(
  points: Iterable[Point]
): Boolean = {

  val seq =
    points.toVector

  seq.length >= 3 &&
  distance(
    seq.head,
    seq.last
  ) <= 45.0
}

// ============================================================
// BOUNDS
// ============================================================

def boundsOf(
  item: DrawItem
): java.awt.Rectangle = {

  if (item.points.isEmpty) {

    new java.awt.Rectangle(
      0,
      0,
      1,
      1
    )

  } else {

    var minX =
      item.points.head.x

    var maxX =
      item.points.head.x

    var minY =
      item.points.head.y

    var maxY =
      item.points.head.y

    var i: Int = 1

    while (i < item.points.length) {

      val p =
        item.points(i)

      if (p.x < minX) {
        minX = p.x
      }

      if (p.x > maxX) {
        maxX = p.x
      }

      if (p.y < minY) {
        minY = p.y
      }

      if (p.y > maxY) {
        maxY = p.y
      }

      i += 1
    }

    new java.awt.Rectangle(
      minX,
      minY,
      math.max(
        1,
        maxX - minX
      ),
      math.max(
        1,
        maxY - minY
      )
    )
  }
}

// ============================================================
// SEGMENT HIT TEST
// ============================================================

def segmentDistance(
  p: Point,
  a: Point,
  b: Point
): Double = {

  val dx: Double =
    b.x - a.x

  val dy: Double =
    b.y - a.y

  if (
    dx == 0.0 &&
    dy == 0.0
  ) {
    distance(
      p,
      a
    )
  } else {

    val dot: Double =
      (p.x - a.x) * dx +
      (p.y - a.y) * dy

    val len2: Double =
      dx * dx +
      dy * dy

    val t: Double =
      math.max(
        0.0,
        math.min(
          1.0,
          dot / len2
        )
      )

    val cx: Double =
      a.x + t * dx

    val cy: Double =
      a.y + t * dy

    math.hypot(
      p.x - cx,
      p.y - cy
    )
  }
}

// ============================================================
// POLYGON HIT TEST
// ============================================================

def insidePolygon(
  p: Point,
  points: Seq[Point]
): Boolean = {

  if (points.length < 3) {
    false
  } else {

    var inside: Boolean =
      false

    var j: Int =
      points.length - 1

    var i: Int =
      0

    while (i < points.length) {

      val xi =
        points(i).x

      val yi =
        points(i).y

      val xj =
        points(j).x

      val yj =
        points(j).y

      val hit =
        ((yi > p.y) != (yj > p.y)) &&
        (
          p.x <
          (
            xj - xi
          ).toDouble *
          (
            p.y - yi
          ).toDouble /
          (
            yj - yi +
            0.000001
          ) +
          xi
        )

      if (hit) {
        inside =
          !inside
      }

      j =
        i

      i += 1
    }

    inside
  }
}

// ============================================================
// OBJECT HIT TEST
// ============================================================

def findObject(
  p: Point
): Int = {

  var result: Int =
    -1

  var i: Int =
    drawItems.length - 1

  while (
    i >= 0 &&
    result < 0
  ) {

    val item =
      drawItems(i)

    if (item.visible) {

      val r =
        boundsOf(item)

      val expanded =
        new java.awt.Rectangle(
          r.x - 14,
          r.y - 14,
          r.width + 28,
          r.height + 28
        )

      if (expanded.contains(p)) {

        if (
          item.filled &&
          closed(item.points) &&
          insidePolygon(
            p,
            item.points.toSeq
          )
        ) {

          result =
            i

        } else {

          var j: Int =
            1

          while (
            j < item.points.length &&
            result < 0
          ) {

            if (
              segmentDistance(
                p,
                item.points(j - 1),
                item.points(j)
              ) < 16.0
            ) {
              result =
                i
            }

            j += 1
          }
        }
      }
    }

    i -= 1
  }

  result
}

// ============================================================
// RESIZE HANDLES
// ============================================================

def handles(
  r: java.awt.Rectangle
): Array[(String, Point)] = {

  val mx =
    r.x + r.width / 2

  val my =
    r.y + r.height / 2

  Array(
    ("TL", new Point(r.x, r.y)),
    ("TM", new Point(mx, r.y)),
    ("TR", new Point(r.x + r.width, r.y)),
    ("ML", new Point(r.x, my)),
    ("MR", new Point(r.x + r.width, my)),
    ("BL", new Point(r.x, r.y + r.height)),
    ("BM", new Point(mx, r.y + r.height)),
    ("BR", new Point(r.x + r.width, r.y + r.height))
  )
}

def resizeHandle(
  item: DrawItem,
  p: Point
): String = {

  val r =
    boundsOf(item)

  val expanded =
    new java.awt.Rectangle(
      r.x - 8,
      r.y - 8,
      r.width + 16,
      r.height + 16
    )

  val hs =
    handles(expanded)

  var result =
    ""

  var i: Int =
    0

  while (
    i < hs.length &&
    result == ""
  ) {

    if (
      distance(
        p,
        hs(i)._2
      ) <= 13.0
    ) {

      result =
        hs(i)._1
    }

    i += 1
  }

  result
}

// ============================================================
// MOVE
// ============================================================

def moveSelected(
  dx: Int,
  dy: Int
): Unit = {

  if (
    selectedIndex >= 0 &&
    selectedIndex < drawItems.length
  ) {

    val item =
      drawItems(selectedIndex)

    if (!item.locked) {

      val moved =
        item.points.map { p =>
          new Point(
            p.x + dx,
            p.y + dy
          )
        }

      drawItems(selectedIndex) =
        item.copy(
          points = moved
        )
    }
  }
}

// ============================================================
// RESIZE
// ============================================================

def resizeSelected(
  p: Point
): Unit = {

  if (
    selectedIndex >= 0 &&
    selectedIndex < drawItems.length &&
    originalPoints.nonEmpty
  ) {

    val item =
      drawItems(selectedIndex)

    var minX =
      originalPoints.head.x

    var maxX =
      originalPoints.head.x

    var minY =
      originalPoints.head.y

    var maxY =
      originalPoints.head.y

    var i: Int =
      1

    while (
      i < originalPoints.length
    ) {

      val q =
        originalPoints(i)

      if (q.x < minX) {
        minX =
          q.x
      }

      if (q.x > maxX) {
        maxX =
          q.x
      }

      if (q.y < minY) {
        minY =
          q.y
      }

      if (q.y > maxY) {
        maxY =
          q.y
      }

      i += 1
    }

    val oldW =
      math.max(
        1,
        maxX - minX
      )

    val oldH =
      math.max(
        1,
        maxY - minY
      )

    var newMinX =
      minX

    var newMaxX =
      maxX

    var newMinY =
      minY

    var newMaxY =
      maxY

    if (resizeHandle == "TL") {
      newMinX = p.x
      newMinY = p.y

    } else if (resizeHandle == "TM") {
      newMinY = p.y

    } else if (resizeHandle == "TR") {
      newMaxX = p.x
      newMinY = p.y

    } else if (resizeHandle == "ML") {
      newMinX = p.x

    } else if (resizeHandle == "MR") {
      newMaxX = p.x

    } else if (resizeHandle == "BL") {
      newMinX = p.x
      newMaxY = p.y

    } else if (resizeHandle == "BM") {
      newMaxY = p.y

    } else if (resizeHandle == "BR") {
      newMaxX = p.x
      newMaxY = p.y
    }

    val newW =
      math.max(
        5,
        newMaxX - newMinX
      )

    val newH =
      math.max(
        5,
        newMaxY - newMinY
      )

    val scaled =
      originalPoints.map { q =>

        val nx =
          newMinX +
          (
            (
              q.x - minX
            ).toDouble *
            newW /
            oldW
          ).toInt

        val ny =
          newMinY +
          (
            (
              q.y - minY
            ).toDouble *
            newH /
            oldH
          ).toInt

        new Point(
          nx,
          ny
        )
      }

    drawItems(selectedIndex) =
      item.copy(
        points = scaled
      )
  }
}

// ============================================================
// GEOMETRY
// ============================================================

def polygon(
  sides: Int,
  radius: Int,
  start: Double
): Vector[Point] = {

  val out =
    ArrayBuffer[Point]()

  var i: Int =
    0

  while (i <= sides) {

    val angle =
      start +
      i.toDouble *
      2.0 *
      math.Pi /
      sides.toDouble

    out +=
      new Point(
        CENTER_X +
        (
          math.cos(angle) *
          radius
        ).toInt,
        CENTER_Y +
        (
          math.sin(angle) *
          radius
        ).toInt
      )

    i += 1
  }

  out.toVector
}

def ellipse(
  rx: Int,
  ry: Int
): Vector[Point] = {

  val out =
    ArrayBuffer[Point]()

  var deg: Int =
    0

  while (deg <= 360) {

    val a =
      deg *
      math.Pi /
      180.0

    out +=
      new Point(
        CENTER_X +
        (
          math.cos(a) *
          rx
        ).toInt,
        CENTER_Y +
        (
          math.sin(a) *
          ry
        ).toInt
      )

    deg += 3
  }

  out.toVector
}

def star(
  count: Int,
  radius: Int
): Vector[Point] = {

  val out =
    ArrayBuffer[Point]()

  var i: Int =
    0

  while (
    i <= count * 2
  ) {

    val angle =
      -math.Pi / 2.0 +
      i.toDouble *
      math.Pi /
      count.toDouble

    val rr =
      if (i % 2 == 0) {
        radius
      } else {
        (radius * 0.45).toInt
      }

    out +=
      new Point(
        CENTER_X +
        (
          math.cos(angle) *
          rr
        ).toInt,
        CENTER_Y +
        (
          math.sin(angle) *
          rr
        ).toInt
      )

    i += 1
  }

  out.toVector
}

def heart(): Vector[Point] = {

  val out =
    ArrayBuffer[Point]()

  var i: Int =
    0

  while (i <= 180) {

    val t =
      i *
      math.Pi /
      180.0

    val x =
      16.0 *
      math.pow(
        math.sin(t),
        3
      )

    val y =
      13.0 *
      math.cos(t) -
      5.0 *
      math.cos(2.0 * t) -
      2.0 *
      math.cos(3.0 * t) -
      math.cos(4.0 * t)

    out +=
      new Point(
        CENTER_X +
        (x * 13.0).toInt,
        CENTER_Y -
        (y * 13.0).toInt
      )

    i += 1
  }

  out.toVector
}

// ============================================================
// SHAPE LIBRARY
// ============================================================

val shapes =
  Array(
    "LINE",
    "SQUARE",
    "RECTANGLE",
    "CIRCLE",
    "ELLIPSE",
    "TRIANGLE",
    "DIAMOND",
    "PENTAGON",
    "HEXAGON",
    "OCTAGON",
    "STAR",
    "STAR 6",
    "HEART",
    "ARROW",
    "PLUS",
    "CLOUD",
    "SUN",
    "MOON",
    "RING",
    "WAVE",
    "SPIRAL",
    "BURST",
    "SNOWFLAKE"
  )

var shapeIndex: Int =
  0

val shapeModel =
  new DefaultListModel[String]()

while (
  shapeIndex < shapes.length
) {

  shapeModel.addElement(
    shapes(shapeIndex)
  )

  shapeIndex += 1
}

// ============================================================
// SHAPE GENERATOR
// ============================================================

def shapeGeometry(
  name: String
): Vector[Point] = {

  val n =
    name.toUpperCase

  if (n == "LINE") {

    Vector(
      new Point(100,120),
      new Point(900,530)
    )

  } else if (n == "SQUARE") {

    Vector(
      new Point(300,150),
      new Point(700,150),
      new Point(700,500),
      new Point(300,500),
      new Point(300,150)
    )

  } else if (n == "RECTANGLE") {

    Vector(
      new Point(170,210),
      new Point(830,210),
      new Point(830,440),
      new Point(170,440),
      new Point(170,210)
    )

  } else if (n == "CIRCLE") {

    ellipse(
      210,
      210
    )

  } else if (n == "ELLIPSE") {

    ellipse(
      290,
      160
    )

  } else if (n == "TRIANGLE") {

    polygon(
      3,
      250,
      -math.Pi / 2.0
    )

  } else if (n == "DIAMOND") {

    Vector(
      new Point(CENTER_X,70),
      new Point(860,CENTER_Y),
      new Point(CENTER_X,580),
      new Point(140,CENTER_Y),
      new Point(CENTER_X,70)
    )

  } else if (n == "PENTAGON") {

    polygon(
      5,
      240,
      -math.Pi / 2.0
    )

  } else if (n == "HEXAGON") {

    polygon(
      6,
      235,
      0.0
    )

  } else if (n == "OCTAGON") {

    polygon(
      8,
      235,
      math.Pi / 8.0
    )

  } else if (n == "STAR") {

    star(
      5,
      250
    )

  } else if (n == "STAR 6") {

    star(
      6,
      250
    )

  } else if (n == "HEART") {

    heart()

  } else if (n == "ARROW") {

    Vector(
      new Point(130,245),
      new Point(620,245),
      new Point(620,135),
      new Point(870,325),
      new Point(620,515),
      new Point(620,405),
      new Point(130,405),
      new Point(130,245)
    )

  } else if (n == "PLUS") {

    Vector(
      new Point(430,90),
      new Point(570,90),
      new Point(570,240),
      new Point(720,240),
      new Point(720,410),
      new Point(570,410),
      new Point(570,560),
      new Point(430,560),
      new Point(430,410),
      new Point(280,410),
      new Point(280,240),
      new Point(430,240),
      new Point(430,90)
    )

  } else if (n == "CLOUD") {

    Vector(
      new Point(160,470),
      new Point(200,360),
      new Point(330,270),
      new Point(450,300),
      new Point(535,195),
      new Point(665,220),
      new Point(790,320),
      new Point(840,390),
      new Point(810,470),
      new Point(160,470)
    )

  } else if (n == "SUN") {

    star(
      16,
      245
    )

  } else if (n == "MOON") {

    ellipse(
      225,
      225
    )

  } else if (n == "RING") {

    ellipse(
      225,
      225
    )

  } else if (n == "WAVE") {

    val out =
      ArrayBuffer[Point]()

    var x: Int =
      30

    while (x <= 970) {

      out +=
        new Point(
          x,
          CENTER_Y +
          (
            math.sin(
              (x - 30) * 0.045
            ) * 100.0
          ).toInt
        )

      x += 5
    }

    out.toVector

  } else if (n == "SPIRAL") {

    val out =
      ArrayBuffer[Point]()

    var angle: Double =
      0.0

    var radius: Double =
      4.0

    while (
      angle < math.Pi * 8.0
    ) {

      out +=
        new Point(
          CENTER_X +
          (
            math.cos(angle) *
            radius
          ).toInt,
          CENTER_Y +
          (
            math.sin(angle) *
            radius
          ).toInt
        )

      angle += 0.08
      radius += 1.5
    }

    out.toVector

  } else if (n == "BURST") {

    star(
      20,
      250
    )

  } else if (n == "SNOWFLAKE") {

    Vector(
      new Point(CENTER_X,70),
      new Point(CENTER_X,580),
      new Point(240,160),
      new Point(760,490),
      new Point(240,490),
      new Point(760,160)
    )

  } else {

    ellipse(
      220,
      160
    )
  }
}

// ============================================================
// OBJECT LIBRARY
// ============================================================

val objects =
  Array(
    "CAR",
    "SPORTS CAR",
    "TRUCK",
    "BUS",
    "TRAIN",
    "AIRPLANE",
    "JET",
    "HELICOPTER",
    "ROCKET",
    "BICYCLE",
    "MOTORBIKE",
    "BOAT",
    "SHIP",
    "SUBMARINE",
    "DRONE",
    "UFO",
    "HOUSE",
    "VILLA",
    "CASTLE",
    "TOWER",
    "BRIDGE",
    "CITY",
    "TREE",
    "PALM TREE",
    "FLOWER",
    "CACTUS",
    "MOUNTAIN",
    "VOLCANO",
    "ISLAND",
    "RAINBOW",
    "SUN",
    "MOON",
    "PLANET",
    "EARTH",
    "SATURN",
    "BOY",
    "GIRL",
    "PERSON",
    "ROBOT",
    "ASTRONAUT",
    "KING",
    "QUEEN",
    "CAT",
    "DOG",
    "BIRD",
    "FISH",
    "SHARK",
    "WHALE",
    "DOLPHIN",
    "LION",
    "TIGER",
    "ELEPHANT",
    "HORSE",
    "RABBIT",
    "PANDA",
    "MONKEY",
    "FOX",
    "DEER",
    "BUTTERFLY",
    "BEE",
    "SNAKE",
    "TURTLE",
    "APPLE",
    "BANANA",
    "MANGO",
    "PIZZA",
    "BURGER",
    "CAKE",
    "GUITAR",
    "PIANO",
    "CAMERA",
    "COMPUTER",
    "PHONE",
    "BOOK",
    "CLOCK",
    "KEY",
    "LAMP",
    "CHAIR",
    "TABLE",
    "STAR",
    "HEART",
    "DIAMOND",
    "CROWN",
    "TROPHY",
    "GIFT",
    "BALLOON",
    "UMBRELLA",
    "FOOTBALL",
    "BASKETBALL",
    "CRICKET BAT",
    "CRICKET BALL",
    "FUTURE CAR",
    "CYBER CITY",
    "NEON TOWER",
    "TIME MACHINE"
  )

val objectModel =
  new DefaultListModel[String]()

var objectInit: Int =
  0

while (
  objectInit < objects.length
) {

  objectModel.addElement(
    objects(objectInit)
  )

  objectInit += 1
}

// ============================================================
// OBJECT GEOMETRY
// ============================================================

def objectGeometry(
  name: String
): Vector[Point] = {

  val n =
    name.toUpperCase

  if (n.indexOf("CAR") >= 0) {

    Vector(
      new Point(100,470),
      new Point(215,365),
      new Point(390,355),
      new Point(470,245),
      new Point(660,245),
      new Point(775,355),
      new Point(900,370),
      new Point(950,470),
      new Point(100,470)
    )

  } else if (
    n == "HOUSE" ||
    n == "VILLA" ||
    n == "CASTLE"
  ) {

    Vector(
      new Point(220,555),
      new Point(220,300),
      new Point(500,70),
      new Point(780,300),
      new Point(780,555),
      new Point(220,555)
    )

  } else if (
    n.indexOf("TREE") >= 0
  ) {

    Vector(
      new Point(455,580),
      new Point(545,580),
      new Point(545,350),
      new Point(680,270),
      new Point(600,150),
      new Point(500,215),
      new Point(400,150),
      new Point(320,270),
      new Point(455,350),
      new Point(455,580)
    )

  } else if (n == "BIRD") {

    Vector(
      new Point(180,350),
      new Point(365,215),
      new Point(500,300),
      new Point(660,180),
      new Point(820,330),
      new Point(630,415),
      new Point(450,400),
      new Point(310,470),
      new Point(180,350)
    )

  } else if (
    n == "FISH" ||
    n == "SHARK" ||
    n == "WHALE" ||
    n == "DOLPHIN"
  ) {

    Vector(
      new Point(120,325),
      new Point(300,210),
      new Point(650,210),
      new Point(865,325),
      new Point(650,440),
      new Point(300,440),
      new Point(120,325)
    )

  } else if (
    n.indexOf("ROCKET") >= 0 ||
    n == "UFO"
  ) {

    Vector(
      new Point(425,525),
      new Point(450,220),
      new Point(500,60),
      new Point(550,220),
      new Point(575,525),
      new Point(500,600),
      new Point(425,525)
    )

  } else if (
    n.indexOf("ROBOT") >= 0
  ) {

    Vector(
      new Point(350,555),
      new Point(350,300),
      new Point(330,300),
      new Point(330,130),
      new Point(670,130),
      new Point(670,300),
      new Point(650,300),
      new Point(650,555),
      new Point(350,555)
    )

  } else if (
    n.indexOf("MOUNTAIN") >= 0
  ) {

    Vector(
      new Point(40,560),
      new Point(280,205),
      new Point(430,400),
      new Point(620,75),
      new Point(960,560),
      new Point(40,560)
    )

  } else if (n == "FLOWER") {

    Vector(
      new Point(500,585),
      new Point(500,315),
      new Point(390,225),
      new Point(500,115),
      new Point(610,225),
      new Point(500,315),
      new Point(500,585)
    )

  } else if (n == "STAR") {

    star(
      5,
      250
    )

  } else if (n == "HEART") {

    heart()

  } else if (n == "CROWN") {

    Vector(
      new Point(230,490),
      new Point(195,155),
      new Point(390,295),
      new Point(500,80),
      new Point(610,295),
      new Point(805,155),
      new Point(770,490),
      new Point(230,490)
    )

  } else {

    polygon(
      6,
      230,
      0.0
    )
  }
}

// ============================================================
// ADD OBJECTS
// ============================================================

def addShape(
  name: String
): Unit = {

  saveHistory()

  val pts =
    shapeGeometry(name)

  drawItems +=
    DrawItem(
      pts,
      strokeColor,
      fillColor,
      closed(pts),
      name,
      true,
      false
    )

  selectedIndex =
    drawItems.length - 1

  activeTool =
    "SELECT"

  repaintCanvas()

  setStatus(
    "SHAPE ADDED: " + name
  )
}

def addObject(
  name: String
): Unit = {

  saveHistory()

  val pts =
    objectGeometry(name)

  drawItems +=
    DrawItem(
      pts,
      strokeColor,
      fillColor,
      closed(pts),
      name,
      true,
      false
    )

  selectedIndex =
    drawItems.length - 1

  activeTool =
    "SELECT"

  repaintCanvas()

  setStatus(
    "OBJECT ADDED: " + name
  )
}

// ============================================================
// FILL
// ============================================================

def fillSelected(): Unit = {

  if (
    selectedIndex >= 0 &&
    selectedIndex < drawItems.length
  ) {

    val item =
      drawItems(selectedIndex)

    if (
      closed(item.points)
    ) {

      saveHistory()

      drawItems(selectedIndex) =
        item.copy(
          fill = fillColor,
          filled = true
        )

      repaintCanvas()

      setStatus(
        "FILL COMPLETE"
      )

    } else {

      setStatus(
        "CLOSED SHAPE REQUIRED"
      )
    }

  } else {

    setStatus(
      "SELECT OBJECT FIRST"
    )
  }
}

// ============================================================
// DELETE / CLEAR
// ============================================================

def deleteSelected(): Unit = {

  if (
    selectedIndex >= 0 &&
    selectedIndex < drawItems.length
  ) {

    saveHistory()

    drawItems.remove(
      selectedIndex
    )

    selectedIndex =
      -1

    repaintCanvas()

    setStatus(
      "OBJECT DELETED"
    )
  }
}

def clearAll(): Unit = {

  if (drawItems.nonEmpty) {
    saveHistory()
  }

  drawItems.clear()

  tempPoints.clear()

  selectedIndex =
    -1

  if (codeArea != null) {
    codeArea.setText(
      "// CANVAS CLEARED"
    )
  }

  repaintCanvas()

  setStatus(
    "CANVAS CLEARED"
  )
}

// ============================================================
// COLOR NAME
// ============================================================

def colorName(
  c: Color
): String = {

  val r =
    c.getRed

  val g =
    c.getGreen

  val b =
    c.getBlue

  if (
    r > 220 &&
    g < 100 &&
    b < 100
  ) {
    "red"

  } else if (
    r < 100 &&
    g > 170 &&
    b < 120
  ) {
    "green"

  } else if (
    r < 100 &&
    g < 160 &&
    b > 170
  ) {
    "blue"

  } else if (
    r > 220 &&
    g > 170 &&
    b < 130
  ) {
    "yellow"

  } else if (
    r > 220 &&
    g > 100 &&
    b < 120
  ) {
    "orange"

  } else if (
    r > 160 &&
    b > 160
  ) {
    "magenta"

  } else if (
    r > 170 &&
    g > 170 &&
    b > 170
  ) {
    "white"

  } else {
    "black"
  }
}

// ============================================================
// EXACT IMAGE IMPORT + CODE GENERATOR
// ============================================================

def escapeScalaString(text: String): String = {
  if (text == null) ""
  else text.replace("\\", "\\\\").replace("\"", "\\\"")
}

def imageToPngBytes(img: BufferedImage): Array[Byte] = {
  val out = new ByteArrayOutputStream()
  javax.imageio.ImageIO.write(img, "png", out)
  out.toByteArray
}

def base64Lines(data: String, width: Int): String = {
  val sb = new StringBuilder()
  var i = 0
  while (i < data.length) {
    val end = math.min(i + width, data.length)
    sb.append(data.substring(i, end))
    if (end < data.length) sb.append("\n")
    i = end
  }
  sb.toString
}

def generateExactImageCode(img: BufferedImage, title: String): String = {
  if (img == null) return "// NO IMAGE AVAILABLE\n"

  val png = imageToPngBytes(img)
  val encoded = java.util.Base64.getEncoder.encodeToString(png)
  val sb = new StringBuilder()

  sb.append("// ULTRA LEGEND - EXACT EMBEDDED IMAGE CODE\n")
  sb.append("// Source: " + escapeScalaString(title) + "\n")
  sb.append("// Original PNG pixels are embedded as Base64; no source-file path is required.\n")
  sb.append("// Viewer starts maximized and supports FIT / FILL / 100% / ZOOM.\n\n")
  sb.append("import java.awt.BorderLayout\n")
  sb.append("import java.awt.Color\n")
  sb.append("import java.awt.Graphics2D\n")
  sb.append("import java.awt.RenderingHints\n")
  sb.append("import java.awt.event.MouseAdapter\n")
  sb.append("import java.awt.event.MouseEvent\n")
  sb.append("import java.awt.image.BufferedImage\n")
  sb.append("import java.io.ByteArrayInputStream\n")
  sb.append("import java.util.Base64\n")
  sb.append("import javax.imageio.ImageIO\n")
  sb.append("import javax.swing.JButton\n")
  sb.append("import javax.swing.JFrame\n")
  sb.append("import javax.swing.JLabel\n")
  sb.append("import javax.swing.JPanel\n")
  sb.append("import javax.swing.JScrollPane\n")
  sb.append("import javax.swing.SwingConstants\n")
  sb.append("import javax.swing.WindowConstants\n\n")
  sb.append("val imageData = Array(\n")

  var chunkIndex = 0
  while (chunkIndex < encoded.length) {
    val endChunk = math.min(chunkIndex + 120, encoded.length)
    sb.append("  \"")
    sb.append(encoded.substring(chunkIndex, endChunk))
    sb.append("\"")
    if (endChunk < encoded.length) sb.append(",")
    sb.append("\n")
    chunkIndex = endChunk
  }

  sb.append(").mkString\n")
  sb.append("val image = ImageIO.read(new ByteArrayInputStream(Base64.getDecoder.decode(imageData)))\n")
  sb.append("var zoom = 1.0\n")
  sb.append("var mode = \"FIT\"\n")
  sb.append("val w = new JFrame(\"EXACT IMAGE VIEWER - " + escapeScalaString(title) + "\")\n")
  sb.append("w.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)\n")
  sb.append("w.setSize(1280, 820)\n")
  sb.append("w.setLocationRelativeTo(null)\n")
  sb.append("w.setExtendedState(JFrame.MAXIMIZED_BOTH)\n")
  sb.append("val info = new JLabel(\"FIT\", SwingConstants.CENTER)\n")
  sb.append("val view = new JPanel {\n")
  sb.append("  override def paintComponent(graphics: java.awt.Graphics): Unit = {\n")
  sb.append("    super.paintComponent(graphics)\n")
  sb.append("    val g = graphics.asInstanceOf[Graphics2D]\n")
  sb.append("    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)\n")
  sb.append("    g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)\n")
  sb.append("    g.setColor(Color.BLACK)\n")
  sb.append("    g.fillRect(0, 0, getWidth, getHeight)\n")
  sb.append("    val baseScale = math.min(getWidth.toDouble / image.getWidth.toDouble, getHeight.toDouble / image.getHeight.toDouble)\n")
  sb.append("    val scale = if (mode == \"FILL\") math.max(getWidth.toDouble / image.getWidth.toDouble, getHeight.toDouble / image.getHeight.toDouble) else if (mode == \"100%\") 1.0 else baseScale\n")
  sb.append("    val finalScale = math.max(0.05, scale * zoom)\n")
  sb.append("    val dw = math.max(1, math.round(image.getWidth * finalScale).toInt)\n")
  sb.append("    val dh = math.max(1, math.round(image.getHeight * finalScale).toInt)\n")
  sb.append("    val dx = (getWidth - dw) / 2\n")
  sb.append("    val dy = (getHeight - dh) / 2\n")
  sb.append("    g.drawImage(image, dx, dy, dw, dh, null)\n")
  sb.append("  }\n")
  sb.append("}\n")
  sb.append("def setMode(m: String): Unit = { mode = m; zoom = 1.0; info.setText(m); view.repaint() }\n")
  sb.append("val bar = new JPanel(new BorderLayout())\n")
  sb.append("val left = new JPanel()\n")
  sb.append("val fitB = new JButton(\"FIT\")\n")
  sb.append("val fillB = new JButton(\"FILL\")\n")
  sb.append("val oneB = new JButton(\"100%\")\n")
  sb.append("val minusB = new JButton(\"ZOOM -\")\n")
  sb.append("val plusB = new JButton(\"ZOOM +\")\n")
  sb.append("fitB.addActionListener(new javax.swing.AbstractAction { override def actionPerformed(e: java.awt.event.ActionEvent): Unit = setMode(\"FIT\") })\n")
  sb.append("fillB.addActionListener(new javax.swing.AbstractAction { override def actionPerformed(e: java.awt.event.ActionEvent): Unit = setMode(\"FILL\") })\n")
  sb.append("oneB.addActionListener(new javax.swing.AbstractAction { override def actionPerformed(e: java.awt.event.ActionEvent): Unit = setMode(\"100%\") })\n")
  sb.append("minusB.addActionListener(new javax.swing.AbstractAction { override def actionPerformed(e: java.awt.event.ActionEvent): Unit = { zoom = math.max(0.1, zoom - 0.1); view.repaint() } })\n")
  sb.append("plusB.addActionListener(new javax.swing.AbstractAction { override def actionPerformed(e: java.awt.event.ActionEvent): Unit = { zoom = math.min(8.0, zoom + 0.1); view.repaint() } })\n")
  sb.append("left.add(fitB); left.add(fillB); left.add(oneB); left.add(minusB); left.add(plusB)\n")
  sb.append("bar.add(left, BorderLayout.WEST)\n")
  sb.append("bar.add(info, BorderLayout.CENTER)\n")
  sb.append("view.addMouseWheelListener(new MouseAdapter { override def mouseWheelMoved(e: java.awt.event.MouseWheelEvent): Unit = { zoom = math.max(0.1, math.min(8.0, zoom - e.getWheelRotation * 0.1)); view.repaint() } })\n")
  sb.append("w.add(bar, BorderLayout.NORTH)\n")
  sb.append("w.add(view, BorderLayout.CENTER)\n")
  sb.append("w.setVisible(true)\n")
  sb.toString
}

def isImageFile(file: java.io.File): Boolean = {
  if (file == null) false
  else {
    val n = file.getName.toLowerCase
    n.endsWith(".png") || n.endsWith(".jpg") || n.endsWith(".jpeg") ||
    n.endsWith(".gif") || n.endsWith(".bmp")
  }
}

def isVideoFile(file: java.io.File): Boolean = {
  if (file == null) false
  else {
    val n = file.getName.toLowerCase
    n.endsWith(".mp4") || n.endsWith(".mov") || n.endsWith(".avi") ||
    n.endsWith(".mkv") || n.endsWith(".webm")
  }
}

def importMediaFile(file: java.io.File): Unit = {
  if (file == null || !file.exists() || !file.isFile) {
    setStatus("IMAGE FILE NOT FOUND")
  } else if (isImageFile(file)) {
    importImageFile(file)
  } else {
    setStatus("IMAGE ONLY: PNG JPG JPEG GIF BMP")
  }
}

def restoreImportedMediaCode(): Unit = {
  if (importedImageOriginal != null) {
    generatedCode = generateExactImageCode(importedImageOriginal, importedImageName)
    if (codeArea != null) codeArea.setText(generatedCode)
    setStatus("IMAGE CODE RESTORED: " + importedImageName)
  } else {
    setStatus("NO IMPORTED IMAGE TO RESTORE")
  }
}

def bufferedImageFromAwtImage(source: java.awt.Image): BufferedImage = {
  if (source == null) return null
  val w = math.max(1, source.getWidth(null))
  val h = math.max(1, source.getHeight(null))
  val out = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB)
  val g = out.createGraphics()
  g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
  g.drawImage(source, 0, 0, null)
  g.dispose()
  out
}

def pasteImageFromClipboard(): Unit = {
  try {
    val cb = Toolkit.getDefaultToolkit.getSystemClipboard
    val t = cb.getContents(null)
    if (t != null && t.isDataFlavorSupported(DataFlavor.imageFlavor)) {
      val raw = t.getTransferData(DataFlavor.imageFlavor).asInstanceOf[java.awt.Image]
      val img = bufferedImageFromAwtImage(raw)
      if (img != null) {
        importedImageOriginal = img
        importedImage = fitImageToCanvas(img)
        importedImagePath = "CLIPBOARD"
        importedImageName = "Clipboard Image"
        generatedCode = generateExactImageCode(importedImageOriginal, importedImageName)
        generatedAnimationCode = ""
        if (codeArea != null) codeArea.setText(generatedCode)
        repaintCanvas()
        setStatus("IMAGE PASTED FROM CLIPBOARD: " + img.getWidth + "x" + img.getHeight)
      } else {
        setStatus("CLIPBOARD IMAGE NOT AVAILABLE")
      }
      return
    }
    setStatus("CLIPBOARD DOES NOT CONTAIN AN IMAGE")
  } catch {
    case ex: Throwable => setStatus("PASTE IMAGE ERROR: " + ex.getMessage)
  }
}

def imageDropHandler(): TransferHandler = {
  new TransferHandler() {
    override def canImport(support: TransferHandler.TransferSupport): Boolean = {
      support.isDataFlavorSupported(DataFlavor.imageFlavor) ||
      support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)
    }

    override def importData(support: TransferHandler.TransferSupport): Boolean = {
      try {
        if (!canImport(support)) return false
        val tr = support.getTransferable

        if (tr.isDataFlavorSupported(DataFlavor.imageFlavor)) {
          val raw = tr.getTransferData(DataFlavor.imageFlavor).asInstanceOf[java.awt.Image]
          val img = bufferedImageFromAwtImage(raw)
          if (img != null) {
            importedImageOriginal = img
            importedImage = fitImageToCanvas(img)
            importedImagePath = "DRAGGED IMAGE"
            importedImageName = "Dropped Image"
            generatedCode = generateExactImageCode(importedImageOriginal, importedImageName)
            generatedAnimationCode = ""
            if (codeArea != null) codeArea.setText(generatedCode)
            repaintCanvas()
            setStatus("IMAGE DROPPED INTO PAINT + EXACT CODE READY")
            true
          } else false
        } else {
          val data = tr.getTransferData(DataFlavor.javaFileListFlavor)
          val files = data.asInstanceOf[java.util.List[_]]
          if (files == null || files.isEmpty) return false
          val it = files.iterator()
          if (it.hasNext) {
            val f = it.next().asInstanceOf[java.io.File]
            if (isImageFile(f)) {
              importImageFile(f)
              true
            } else {
              setStatus("PAINT ACCEPTS IMAGES ONLY")
              false
            }
          } else false
        }
      } catch {
        case ex: Throwable =>
          setStatus("PAINT DROP ERROR: " + ex.getMessage)
          false
      }
    }
  }
}

def mediaDropHandler(): TransferHandler = {
  new TransferHandler() {
    override def canImport(support: TransferHandler.TransferSupport): Boolean = {
      support.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ||
      support.isDataFlavorSupported(DataFlavor.imageFlavor)
    }

    override def importData(support: TransferHandler.TransferSupport): Boolean = {
      try {
        if (!canImport(support)) return false
        val tr = support.getTransferable

        if (tr.isDataFlavorSupported(DataFlavor.imageFlavor)) {
          val raw = tr.getTransferData(DataFlavor.imageFlavor).asInstanceOf[java.awt.Image]
          val img = bufferedImageFromAwtImage(raw)
          if (img != null) {
            importedImageOriginal = img
            importedImage = fitImageToCanvas(img)
            importedImagePath = "DRAGGED CLIPBOARD IMAGE"
            importedImageName = "Dropped Image"
            generatedCode = generateExactImageCode(importedImageOriginal, importedImageName)
            if (codeArea != null) codeArea.setText(generatedCode)
            repaintCanvas()
            setStatus("IMAGE DROPPED + EXACT CODE READY")
            true
          } else false
        } else {
          val data = tr.getTransferData(DataFlavor.javaFileListFlavor)
          val files = data.asInstanceOf[java.util.List[_]]
          if (files == null || files.isEmpty) return false
          val it = files.iterator()
          if (it.hasNext) {
            val f = it.next().asInstanceOf[java.io.File]
            importMediaFile(f)
            true
          } else false
        }
      } catch {
        case ex: Throwable =>
          setStatus("DROP IMPORT ERROR: " + ex.getMessage)
          false
      }
    }
  }
}

def fitImageToCanvas(img: BufferedImage): BufferedImage = {
  if (img == null) return null
  val fit = math.min(
    CANVAS_W.toDouble / img.getWidth.toDouble,
    CANVAS_H.toDouble / img.getHeight.toDouble
  )
  val drawW = math.max(1, math.round(img.getWidth * fit).toInt)
  val drawH = math.max(1, math.round(img.getHeight * fit).toInt)
  val scaled = new BufferedImage(drawW, drawH, BufferedImage.TYPE_INT_ARGB)
  val sg = scaled.createGraphics()
  sg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
  sg.drawImage(img, 0, 0, drawW, drawH, null)
  sg.dispose()
  scaled
}

def importImageFile(file: java.io.File): Unit = {
  try {
    val img = javax.imageio.ImageIO.read(file)
    if (img == null) {
      setStatus("NOT A SUPPORTED IMAGE")
      return
    }

    importedImageOriginal = img
    importedImage = img
    importedImagePath = file.getAbsolutePath
    importedImageName = file.getName

    importedImage = fitImageToCanvas(img)

    repaintCanvas()
    if (codeArea != null) codeArea.setText(generateExactImageCode(img, file.getName))
    generatedCode = generateExactImageCode(img, file.getName)
    setStatus("IMAGE IMPORTED EXACTLY: " + file.getName + " | " + img.getWidth + "x" + img.getHeight)
  } catch {
    case ex: Throwable => setStatus("IMAGE IMPORT ERROR: " + ex.getMessage)
  }
}

def chooseAndImportImage(parent: JFrame): Unit = {
  val chooser = new JFileChooser(defaultMediaDirectory())
  chooser.setDialogTitle("IMPORT IMAGE FROM FILES")
  chooser.setFileFilter(new FileNameExtensionFilter("Images: PNG, JPG, JPEG, GIF, BMP", "png", "jpg", "jpeg", "gif", "bmp"))
  if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
    importImageFile(chooser.getSelectedFile)
  }
}

// ============================================================
// GENERATE KOJO CODE
// ============================================================

def generateCode(): Unit = {

  if (importedImageOriginal != null) {
    generatedCode = generateExactImageCode(
      importedImageOriginal,
      importedImageName
    )
    codeArea.setText(generatedCode)
    setStatus("EXACT IMAGE CODE GENERATED: " + importedImageOriginal.getWidth + "x" + importedImageOriginal.getHeight)
    return
  }

  val sb =
    new StringBuilder()

  sb.append(
    "// ULTRA LEGEND GENERATED KOJO CODE\n"
  )

  sb.append(
    "cleari()\n"
  )

  sb.append(
    "setAnimationDelay(5)\n\n"
  )

  if (drawItems.isEmpty) {

    sb.append(
      "// NOTHING DRAWN\n"
    )

  } else {

    var i: Int =
      0

    while (
      i < drawItems.length
    ) {

      val item =
        drawItems(i)

      if (
        item.visible &&
        item.points.nonEmpty
      ) {

        sb.append(
          "// " +
          item.name +
          "\n"
        )

        sb.append(
          "setPenColor(" +
          colorName(
            item.stroke
          ) +
          ")\n"
        )

        sb.append(
          "penUp()\n"
        )

        sb.append(
          "setPosition(" +
          (
            item.points.head.x -
            CENTER_X
          ) +
          ", " +
          (
            CENTER_Y -
            item.points.head.y
          ) +
          ")\n"
        )

        sb.append(
          "penDown()\n"
        )

        var j: Int =
          1

        while (
          j < item.points.length
        ) {

          sb.append(
            "lineTo(" +
            (
              item.points(j).x -
              CENTER_X
            ) +
            ", " +
            (
              CENTER_Y -
              item.points(j).y
            ) +
            ")\n"
          )

          j += 1
        }

        if (item.filled && item.points.length >= 3) {
          sb.append(
            "setFillColor(" +
            item.fill.getRed + "," +
            item.fill.getGreen + "," +
            item.fill.getBlue +
            ")\n"
          )
          sb.append(
            "// Fill RGB: " +
            item.fill.getRed + "," +
            item.fill.getGreen + "," +
            item.fill.getBlue +
            "\n"
          )
        }

        sb.append(
          "\n"
        )
      }

      i += 1
    }
  }

  generatedCode =
    sb.toString

  codeArea.setText(
    generatedCode
  )

  setStatus(
    "DRAW CODE GENERATED"
  )
}

// ============================================================
// COPY / SAVE
// ============================================================

def copyText(
  text: String
): Unit = {

  try {

    val clipboard =
      java.awt.Toolkit
        .getDefaultToolkit
        .getSystemClipboard

    clipboard.setContents(
      new java.awt.datatransfer.StringSelection(
        text
      ),
      null
    )

    setStatus(
      "COPIED"
    )

  } catch {
    case _: Exception =>
      setStatus(
        "COPY FAILED"
      )
  }
}

def saveText(
  text: String
): Unit = {

  val chooser =
    new JFileChooser()

  chooser.setDialogTitle(
    "SAVE SCALA CODE"
  )

  if (
    chooser.showSaveDialog(
      mainFrame
    ) ==
    JFileChooser.APPROVE_OPTION
  ) {

    try {

      var file =
        chooser.getSelectedFile

      if (
        !file.getName
          .toLowerCase
          .endsWith(".scala")
      ) {

        file =
          new java.io.File(
            file.getAbsolutePath +
            ".scala"
          )
      }

      val out =
        new java.io.PrintWriter(
          file
        )

      out.write(text)
      out.close()

      setStatus(
        "FILE SAVED"
      )

    } catch {

      case _: Exception =>
        setStatus(
          "SAVE FAILED"
        )
    }
  }
}

// ============================================================
// ANIMATION CODE
// ============================================================

def generateAnimationCode(): Unit = {

  val sb =
    new StringBuilder()

  sb.append(
    "// ULTRA LEGEND ANIMATION CODE\n"
  )

  sb.append(
    "cleari()\n"
  )

  sb.append(
    "val p = Picture.circle(40)\n"
  )

  sb.append(
    "p.setPosition(0,0)\n"
  )

  sb.append(
    "draw(p)\n\n"
  )

  sb.append(
    "animate {\n"
  )

  sb.append(
    "  p.forward(3)\n"
  )

  sb.append(
    "  p.rotate(3)\n"
  )

  sb.append(
    "}\n"
  )

  generatedAnimationCode =
    sb.toString

  codeArea.setText(
    generatedAnimationCode
  )

  setStatus(
    "ANIMATION CODE GENERATED"
  )
}

// ============================================================
// GAME DATABASE
// ============================================================


val gameDatabase =
  Array(
    GameInfo(
      "SLING BALL",
      "MOUSE",
      "Drag and release the ball toward the target."
    ),

    GameInfo(
      "PONG",
      "MOUSE",
      "Move the paddle and keep the ball alive."
    ),

    GameInfo(
      "SNAKE",
      "KEYBOARD",
      "Use arrows or WASD to collect food."
    ),

    GameInfo(
      "BREAKOUT",
      "MOUSE",
      "Move the paddle and break blocks."
    ),

    GameInfo(
      "FLAPPY BIRD",
      "MOUSE",
      "Click to flap and pass the obstacles."
    ),

    GameInfo(
      "CAR DODGE",
      "KEYBOARD",
      "Move left and right to avoid traffic."
    ),

    GameInfo(
      "SPACE SHOOTER",
      "KEYBOARD",
      "Move and fire at falling enemies."
    ),

    GameInfo(
      "TARGET RUSH",
      "MOUSE",
      "Click targets as quickly as possible."
    ),

    GameInfo(
      "BALL CATCH",
      "MOUSE",
      "Move the basket and catch falling balls."
    ),

    GameInfo(
      "MAZE BALL",
      "MOUSE",
      "Drag the ball through a maze."
    ),

    GameInfo(
      "TIC TAC TOE",
      "MOUSE",
      "Play against a simple computer."
    ),

    GameInfo(
      "KEYBOARD RUNNER",
      "KEYBOARD",
      "Run, jump, collect coins and avoid enemies."
    ),

    GameInfo(
      "KEYBOARD SPACE",
      "KEYBOARD",
      "Control a spaceship and shoot enemies."
    ),

    GameInfo(
      "PLATFORM QUEST",
      "KEYBOARD",
      "Jump, collect coins and reach the goal."
    ),

    GameInfo(
      "DODGE FIELD",
      "MOUSE",
      "Move around and avoid falling enemies."
    )
  )

// ============================================================
// GAME WINDOW
// ============================================================

def gameFrame(
  title: String
): JFrame = {

  val frame =
    new JFrame(title)

  frame.setDefaultCloseOperation(
    WindowConstants.DISPOSE_ON_CLOSE
  )

  frame.setSize(
    900,
    650
  )

  frame.setLocationRelativeTo(
    mainFrame
  )

  frame
}

// ============================================================
// GAME 1 - SLING BALL
// ============================================================

def playSlingBall(): Unit = {

  val frame =
    gameFrame(
      "SLING BALL - MOUSE"
    )

  var ballX: Double =
    110.0

  var ballY: Double =
    510.0

  var vx: Double =
    0.0

  var vy: Double =
    0.0

  var launched: Boolean =
    false

  var won: Boolean =
    false

  val target =
    new java.awt.Rectangle(
      760,
      90,
      70,
      70
    )

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setPaint(
          new GradientPaint(
            0,
            0,
            new Color(30,100,160),
            0,
            getHeight,
            new Color(10,20,40)
          )
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          Color.RED
        )

        g.fillOval(
          target.x,
          target.y,
          target.width,
          target.height
        )

        g.setColor(
          Color.YELLOW
        )

        g.fillOval(
          ballX.toInt - 10,
          ballY.toInt - 10,
          20,
          20
        )

        g.setColor(
          Color.WHITE
        )

        g.drawString(
          if (won)
            "YOU WIN!"
          else
            "MOVE MOUSE + RELEASE TO LAUNCH",
          20,
          30
        )
      }
    }

  panel.addMouseListener(
    new MouseAdapter {

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

        if (!launched) {

          vx =
            (
              ballX -
              e.getX
            ) * 0.08

          vy =
            (
              ballY -
              e.getY
            ) * 0.08

          launched =
            true
        }
      }
    }
  )

  val timer: Timer =
    new Timer(
      25,
      new ActionListener {

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

          if (
            launched &&
            !won
          ) {

            vy +=
              0.25

            ballX +=
              vx

            ballY +=
              vy

            if (
              ballX < 10 ||
              ballX > frame.getWidth - 10
            ) {
              vx =
                -vx
            }

            if (ballY < 10) {
              vy =
                -vy
            }

            if (
              ballY >
              frame.getHeight + 20
            ) {

              ballX =
                110

              ballY =
                510

              vx =
                0

              vy =
                0

              launched =
                false
            }

            if (
              target.contains(
                ballX.toInt,
                ballY.toInt
              )
            ) {
              won =
                true
            }

            panel.repaint()
          }
        }
      }
    )

  timer.start()

  frame.addWindowListener(
    new java.awt.event.WindowAdapter {

      override def windowClosed(
        e: java.awt.event.WindowEvent
      ): Unit = {
        timer.stop()
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )
}

// ============================================================
// GAME 2 - PONG
// ============================================================

def playPong(): Unit = {

  val frame =
    gameFrame(
      "PONG - MOUSE"
    )

  var paddleX: Double =
    350.0

  var ballX: Double =
    420.0

  var ballY: Double =
    280.0

  var vx: Double =
    5.0

  var vy: Double =
    4.0

  var score: Int =
    0

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setColor(
          Color.BLACK
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          Color.WHITE
        )

        g.fillOval(
          ballX.toInt - 10,
          ballY.toInt - 10,
          20,
          20
        )

        g.fillRoundRect(
          paddleX.toInt,
          getHeight - 60,
          150,
          20,
          10,
          10
        )

        g.drawString(
          "SCORE: " + score,
          20,
          30
        )
      }
    }

  panel.addMouseMotionListener(
    new MouseMotionAdapter {

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

        paddleX =
          math.max(
            0,
            math.min(
              panel.getWidth - 150,
              e.getX - 75
            )
          )
      }
    }
  )

  val pongTimer: Timer =
    new Timer(
      25,
      new ActionListener {

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

          ballX +=
            vx

          ballY +=
            vy

          if (
            ballX < 10 ||
            ballX > panel.getWidth - 10
          ) {
            vx =
              -vx
          }

          if (ballY < 10) {
            vy =
              -vy
          }

          if (
            ballY >
            panel.getHeight - 85 &&
            ballX >= paddleX &&
            ballX <= paddleX + 150
          ) {

            vy =
              -math.abs(vy)

            score +=
              1
          }

          if (
            ballY >
            panel.getHeight
          ) {

            ballX =
              panel.getWidth / 2.0

            ballY =
              panel.getHeight / 2.0

            vx =
              5.0

            vy =
              -4.0

            score =
              0
          }

          panel.repaint()
        }
      }
    )

  pongTimer.start()

  frame.addWindowListener(
    new java.awt.event.WindowAdapter {

      override def windowClosed(
        e: java.awt.event.WindowEvent
      ): Unit = {
        pongTimer.stop()
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )
}

// ============================================================
// GAME 3 - KEYBOARD SNAKE
// ============================================================

def playSnake(): Unit = {

  val frame =
    gameFrame(
      "SNAKE - KEYBOARD"
    )

  val cell: Int =
    25

  val body =
    ArrayBuffer[Point](
      new Point(10,10),
      new Point(9,10),
      new Point(8,10)
    )

  var food =
    new Point(
      16,
      10
    )

  var dirX: Int =
    1

  var dirY: Int =
    0

  var score: Int =
    0

  var running: Boolean =
    true

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setColor(
          new Color(15,50,25)
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          new Color(
            255,
            255,
            255,
            20
          )
        )

        var gx: Int =
          0

        while (
          gx <= getWidth
        ) {

          g.drawLine(
            gx,
            0,
            gx,
            getHeight
          )

          gx +=
            cell
        }

        var gy: Int =
          0

        while (
          gy <= getHeight
        ) {

          g.drawLine(
            0,
            gy,
            getWidth,
            gy
          )

          gy +=
            cell
        }

        g.setColor(
          Color.RED
        )

        g.fillOval(
          food.x * cell + 3,
          food.y * cell + 3,
          cell - 6,
          cell - 6
        )

        var i: Int =
          0

        while (
          i < body.length
        ) {

          g.setColor(
            if (i == 0)
              new Color(80,250,100)
            else
              new Color(35,175,65)
          )

          g.fillRoundRect(
            body(i).x * cell + 2,
            body(i).y * cell + 2,
            cell - 4,
            cell - 4,
            8,
            8
          )

          i +=
            1
        }

        g.setColor(
          Color.WHITE
        )

        g.setFont(
          new Font(
            "Arial",
            Font.BOLD,
            18
          )
        )

        g.drawString(
          "ARROWS / WASD   SCORE: " + score,
          15,
          25
        )

        if (!running) {

          g.setFont(
            new Font(
              "Arial",
              Font.BOLD,
              42
            )
          )

          g.drawString(
            "GAME OVER",
            290,
            300
          )
        }
      }
    }

  panel.setFocusable(
    true
  )

  panel.addKeyListener(
    new KeyAdapter {

      override def keyPressed(
        e: KeyEvent
      ): Unit = {

        val k =
          e.getKeyCode

        if (
          k == KeyEvent.VK_LEFT ||
          k == KeyEvent.VK_A
        ) {

          if (dirX != 1) {
            dirX =
              -1

            dirY =
              0
          }

        } else if (
          k == KeyEvent.VK_RIGHT ||
          k == KeyEvent.VK_D
        ) {

          if (dirX != -1) {
            dirX =
              1

            dirY =
              0
          }

        } else if (
          k == KeyEvent.VK_UP ||
          k == KeyEvent.VK_W
        ) {

          if (dirY != 1) {
            dirX =
              0

            dirY =
              -1
          }

        } else if (
          k == KeyEvent.VK_DOWN ||
          k == KeyEvent.VK_S
        ) {

          if (dirY != -1) {
            dirX =
              0

            dirY =
              1
          }
        }
      }
    }
  )

  val snakeTimer: Timer =
    new Timer(
      110,
      new ActionListener {

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

          if (
            running &&
            body.nonEmpty
          ) {

            val head =
              body.head

            val nx =
              head.x +
              dirX

            val ny =
              head.y +
              dirY

            val cols =
              math.max(
                1,
                panel.getWidth / cell
              )

            val rows =
              math.max(
                1,
                panel.getHeight / cell
              )

            if (
              nx < 0 ||
              ny < 0 ||
              nx >= cols ||
              ny >= rows
            ) {

              running =
                false

            } else {

              val newHead =
                new Point(
                  nx,
                  ny
                )

              var hitSelf: Boolean =
                false

              var i: Int =
                0

              while (
                i < body.length
              ) {

                if (
                  body(i).x == newHead.x &&
                  body(i).y == newHead.y
                ) {
                  hitSelf =
                    true
                }

                i +=
                  1
              }

              if (hitSelf) {

                running =
                  false

              } else {

                body.prepend(
                  newHead
                )

                if (
                  newHead.x == food.x &&
                  newHead.y == food.y
                ) {

                  score +=
                    1

                  var foundFood: Boolean =
                    false

                  while (!foundFood) {

                    val fx =
                      (
                        RANDOM_GEN.nextDouble() *
                        cols
                      ).toInt

                    val fy =
                      (
                        RANDOM_GEN.nextDouble() *
                        rows
                      ).toInt

                    foundFood =
                      true

                    var q: Int =
                      0

                    while (
                      q < body.length
                    ) {

                      if (
                        body(q).x == fx &&
                        body(q).y == fy
                      ) {
                        foundFood =
                          false
                      }

                      q +=
                        1
                    }

                    if (foundFood) {
                      food =
                        new Point(
                          fx,
                          fy
                        )
                    }
                  }

                } else {

                  body.remove(
                    body.length - 1
                  )
                }
              }
            }

            panel.repaint()
          }
        }
      }
    )

  snakeTimer.start()

  frame.addWindowListener(
    new java.awt.event.WindowAdapter {

      override def windowClosed(
        e: java.awt.event.WindowEvent
      ): Unit = {
        snakeTimer.stop()
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )

  panel.requestFocusInWindow()
}

// ============================================================
// GAME 4 - BREAKOUT
// ============================================================

def playBreakout(): Unit = {

  val frame =
    gameFrame(
      "BREAKOUT - MOUSE"
    )

  var paddleX: Double =
    350.0

  var bx: Double =
    450.0

  var by: Double =
    420.0

  var vx: Double =
    4.0

  var vy: Double =
    -4.0

  var score: Int =
    0

  val blocks =
    ArrayBuffer[Point]()

  var r: Int =
    0

  while (
    r < 5
  ) {

    var c: Int =
      0

    while (
      c < 10
    ) {

      blocks +=
        new Point(
          40 + c * 75,
          70 + r * 32
        )

      c +=
        1
    }

    r +=
      1
  }

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setColor(
          new Color(18,18,40)
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          Color.WHITE
        )

        g.fillOval(
          bx.toInt - 9,
          by.toInt - 9,
          18,
          18
        )

        g.setColor(
          Color.CYAN
        )

        g.fillRoundRect(
          paddleX.toInt,
          getHeight - 55,
          150,
          18,
          10,
          10
        )

        g.setColor(
          Color.ORANGE
        )

        var i: Int =
          0

        while (
          i < blocks.length
        ) {

          g.fillRect(
            blocks(i).x,
            blocks(i).y,
            65,
            22
          )

          i +=
            1
        }

        g.setColor(
          Color.WHITE
        )

        g.drawString(
          "SCORE: " + score,
          20,
          28
        )
      }
    }

  panel.addMouseMotionListener(
    new MouseMotionAdapter {

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

        paddleX =
          math.max(
            0,
            math.min(
              panel.getWidth - 150,
              e.getX - 75
            )
          )
      }
    }
  )

  val breakoutTimer: Timer =
    new Timer(
      25,
      new ActionListener {

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

          bx +=
            vx

          by +=
            vy

          if (
            bx < 10 ||
            bx > panel.getWidth - 10
          ) {
            vx =
              -vx
          }

          if (by < 10) {
            vy =
              -vy
          }

          if (
            by >
            panel.getHeight - 75 &&
            bx >= paddleX &&
            bx <= paddleX + 150
          ) {
            vy =
              -math.abs(vy)
          }

          var hitBlock: Int =
            -1

          var i: Int =
            0

          while (
            i < blocks.length
          ) {

            if (
              bx >= blocks(i).x &&
              bx <= blocks(i).x + 65 &&
              by >= blocks(i).y &&
              by <= blocks(i).y + 22
            ) {

              hitBlock =
                i
            }

            i +=
              1
          }

          if (
            hitBlock >= 0
          ) {

            blocks.remove(
              hitBlock
            )

            vy =
              -vy

            score +=
              1
          }

          if (
            by >
            panel.getHeight
          ) {

            bx =
              panel.getWidth / 2.0

            by =
              panel.getHeight / 2.0

            vx =
              4.0

            vy =
              -4.0

            score =
              0
          }

          panel.repaint()
        }
      }
    )

  breakoutTimer.start()

  frame.addWindowListener(
    new java.awt.event.WindowAdapter {

      override def windowClosed(
        e: java.awt.event.WindowEvent
      ): Unit = {
        breakoutTimer.stop()
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )
}

// ============================================================
// GAME 5 - FLAPPY
// ============================================================

def playFlappy(): Unit = {

  val frame =
    gameFrame(
      "FLAPPY BIRD - MOUSE"
    )

  var birdY: Double =
    280.0

  var birdV: Double =
    0.0

  var pipeX: Double =
    850.0

  var gap: Double =
    290.0

  var score: Int =
    0

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setPaint(
          new GradientPaint(
            0,
            0,
            new Color(70,190,255),
            0,
            getHeight,
            Color.WHITE
          )
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          Color.YELLOW
        )

        g.fillOval(
          150,
          birdY.toInt,
          45,
          35
        )

        g.setColor(
          Color.GREEN
        )

        g.fillRect(
          pipeX.toInt,
          0,
          70,
          gap.toInt - 85
        )

        g.fillRect(
          pipeX.toInt,
          gap.toInt + 85,
          70,
          getHeight
        )

        g.setColor(
          Color.BLACK
        )

        g.drawString(
          "CLICK TO FLAP   SCORE: " + score,
          20,
          30
        )
      }
    }

  panel.addMouseListener(
    new MouseAdapter {

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

        birdV =
          -9.0
      }
    }
  )

  val flappyTimer: Timer =
    new Timer(
      25,
      new ActionListener {

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

          birdV +=
            0.4

          birdY +=
            birdV

          pipeX -=
            5.0

          if (
            pipeX <
            -80
          ) {

            pipeX =
              panel.getWidth + 20

            gap =
              170 +
              RANDOM_GEN.nextDouble() *
              220

            score +=
              1
          }

          if (
            birdY < 0 ||
            birdY > panel.getHeight
          ) {

            birdY =
              280

            birdV =
              0

            pipeX =
              panel.getWidth

            score =
              0
          }

          panel.repaint()
        }
      }
    )

  flappyTimer.start()

  frame.addWindowListener(
    new java.awt.event.WindowAdapter {

      override def windowClosed(
        e: java.awt.event.WindowEvent
      ): Unit = {
        flappyTimer.stop()
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )
}

// ============================================================
// GAME 6 - KEYBOARD CAR
// ============================================================

def playKeyboardCar(): Unit = {

  val frame =
    gameFrame(
      "CAR DODGE - KEYBOARD"
    )

  var playerX: Double =
    420.0

  var enemyX: Double =
    240.0

  var enemyY: Double =
    -120.0

  var score: Int =
    0

  var running: Boolean =
    true

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setColor(
          new Color(45,45,50)
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          Color.WHITE
        )

        var roadY: Int =
          0

        while (
          roadY < getHeight
        ) {

          g.fillRect(
            getWidth / 2 - 5,
            roadY,
            10,
            60
          )

          roadY +=
            100
        }

        g.setColor(
          Color.BLUE
        )

        g.fillRoundRect(
          playerX.toInt,
          getHeight - 125,
          60,
          100,
          18,
          18
        )

        g.setColor(
          Color.RED
        )

        g.fillRoundRect(
          enemyX.toInt,
          enemyY.toInt,
          60,
          100,
          18,
          18
        )

        g.setColor(
          Color.WHITE
        )

        g.drawString(
          "LEFT / RIGHT   SCORE: " + score,
          15,
          28
        )

        if (!running) {

          g.setFont(
            new Font(
              "Arial",
              Font.BOLD,
              42
            )
          )

          g.drawString(
            "GAME OVER",
            300,
            300
          )
        }
      }
    }

  panel.setFocusable(
    true
  )

  panel.addKeyListener(
    new KeyAdapter {

      override def keyPressed(
        e: KeyEvent
      ): Unit = {

        val k =
          e.getKeyCode

        if (
          k == KeyEvent.VK_LEFT ||
          k == KeyEvent.VK_A
        ) {
          playerX -=
            15
        } else if (
          k == KeyEvent.VK_RIGHT ||
          k == KeyEvent.VK_D
        ) {
          playerX +=
            15
        }

        playerX =
          math.max(
            0,
            math.min(
              panel.getWidth - 60,
              playerX
            )
          )
      }
    }
  )

  val carTimer: Timer =
    new Timer(
      25,
      new ActionListener {

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

          if (running) {

            enemyY +=
              7

            if (
              enemyY >
              panel.getHeight
            ) {

              enemyY =
                -100

              enemyX =
                RANDOM_GEN.nextDouble() *
                math.max(
                  1,
                  panel.getWidth - 60
                )

              score +=
                1
            }

            val hit =
              enemyX + 60 >
              playerX &&
              enemyX <
              playerX + 60 &&
              enemyY + 100 >
              panel.getHeight - 125 &&
              enemyY <
              panel.getHeight

            if (hit) {
              running =
                false
            }

            panel.repaint()
          }
        }
      }
    )

  carTimer.start()

  frame.addWindowListener(
    new java.awt.event.WindowAdapter {

      override def windowClosed(
        e: java.awt.event.WindowEvent
      ): Unit = {
        carTimer.stop()
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )

  panel.requestFocusInWindow()
}

// ============================================================
// GAME 7 - KEYBOARD SPACE
// ============================================================

def playKeyboardSpace(): Unit = {

  val frame =
    gameFrame(
      "SPACE SHOOTER - KEYBOARD"
    )

  var shipX: Double =
    420.0

  var enemyX: Double =
    350.0

  var enemyY: Double =
    60.0

  var laserX: Double =
    -100.0

  var laserY: Double =
    -100.0

  var score: Int =
    0

  var running: Boolean =
    true

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setColor(
          new Color(5,10,30)
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          Color.WHITE
        )

        var s: Int =
          0

        while (
          s < 90
        ) {

          g.fillOval(
            (
              s * 83
            ) %
            math.max(
              1,
              getWidth
            ),
            (
              s * 47
            ) %
            math.max(
              1,
              getHeight
            ),
            2,
            2
          )

          s +=
            1
        }

        g.setColor(
          Color.CYAN
        )

        val ship =
          new java.awt.Polygon()

        ship.addPoint(
          shipX.toInt,
          getHeight - 70
        )

        ship.addPoint(
          shipX.toInt + 25,
          getHeight - 130
        )

        ship.addPoint(
          shipX.toInt + 50,
          getHeight - 70
        )

        g.fillPolygon(
          ship
        )

        g.setColor(
          Color.RED
        )

        g.fillOval(
          enemyX.toInt,
          enemyY.toInt,
          45,
          35
        )

        if (
          laserY >
          -40
        ) {

          g.setColor(
            Color.YELLOW
          )

          g.fillRect(
            laserX.toInt,
            laserY.toInt,
            6,
            20
          )
        }

        g.setColor(
          Color.WHITE
        )

        g.drawString(
          "LEFT / RIGHT + SPACE   SCORE: " +
          score,
          15,
          28
        )

        if (!running) {

          g.setFont(
            new Font(
              "Arial",
              Font.BOLD,
              38
            )
          )

          g.drawString(
            "MISSION OVER",
            285,
            300
          )
        }
      }
    }

  panel.setFocusable(
    true
  )

  panel.addKeyListener(
    new KeyAdapter {

      override def keyPressed(
        e: KeyEvent
      ): Unit = {

        val k =
          e.getKeyCode

        if (
          k == KeyEvent.VK_LEFT ||
          k == KeyEvent.VK_A
        ) {
          shipX -=
            12
        } else if (
          k == KeyEvent.VK_RIGHT ||
          k == KeyEvent.VK_D
        ) {
          shipX +=
            12
        } else if (
          k == KeyEvent.VK_SPACE
        ) {

          laserX =
            shipX + 22

          laserY =
            panel.getHeight - 150
        }

        shipX =
          math.max(
            0,
            math.min(
              panel.getWidth - 50,
              shipX
            )
          )
      }
    }
  )

  val spaceTimer: Timer =
    new Timer(
      25,
      new ActionListener {

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

          if (running) {

            enemyY +=
              2

            laserY -=
              12

            if (
              laserX >= enemyX &&
              laserX <= enemyX + 45 &&
              laserY <= enemyY + 35 &&
              laserY >= enemyY - 20
            ) {

              score +=
                1

              enemyX =
                RANDOM_GEN.nextDouble() *
                math.max(
                  1,
                  panel.getWidth - 45
                )

              enemyY =
                50
            }

            if (
              enemyY >
              panel.getHeight
            ) {
              enemyY =
                50
            }

            panel.repaint()
          }
        }
      }
    )

  spaceTimer.start()

  frame.addWindowListener(
    new java.awt.event.WindowAdapter {

      override def windowClosed(
        e: java.awt.event.WindowEvent
      ): Unit = {
        spaceTimer.stop()
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )

  panel.requestFocusInWindow()
}

// ============================================================
// GAME 8 - TARGET RUSH
// ============================================================

def playTargetRush(): Unit = {

  val frame =
    gameFrame(
      "TARGET RUSH - MOUSE"
    )

  var tx: Double =
    350.0

  var ty: Double =
    250.0

  var score: Int =
    0

  var timeLeft: Int =
    20

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setColor(
          new Color(20,25,60)
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          Color.RED
        )

        g.fillOval(
          tx.toInt,
          ty.toInt,
          80,
          80
        )

        g.setColor(
          Color.WHITE
        )

        g.drawOval(
          tx.toInt + 18,
          ty.toInt + 18,
          44,
          44
        )

        g.setFont(
          new Font(
            "Arial",
            Font.BOLD,
            20
          )
        )

        g.drawString(
          "SCORE: " +
          score +
          "   TIME: " +
          timeLeft,
          20,
          30
        )
      }
    }

  panel.addMouseListener(
    new MouseAdapter {

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

        if (
          e.getX >= tx &&
          e.getX <= tx + 80 &&
          e.getY >= ty &&
          e.getY <= ty + 80 &&
          timeLeft > 0
        ) {

          score +=
            1

          tx =
            RANDOM_GEN.nextDouble() *
            math.max(
              1,
              panel.getWidth - 80
            )

          ty =
            70 +
            RANDOM_GEN.nextDouble() *
            math.max(
              1,
              panel.getHeight - 150
            )

          panel.repaint()
        }
      }
    }
  )

  val targetTimer: Timer =
    new Timer(
      1000,
      new ActionListener {

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

          timeLeft -=
            1

          if (
            timeLeft <=
            0
          ) {

            timeLeft =
              20

            score =
              0
          }

          panel.repaint()
        }
      }
    )

  targetTimer.start()

  frame.addWindowListener(
    new java.awt.event.WindowAdapter {

      override def windowClosed(
        e: java.awt.event.WindowEvent
      ): Unit = {
        targetTimer.stop()
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )
}

// ============================================================
// GAME 9 - BALL CATCH
// ============================================================

def playBallCatch(): Unit = {

  val frame =
    gameFrame(
      "BALL CATCH - MOUSE"
    )

  var basketX: Double =
    350.0

  var ballX: Double =
    200.0

  var ballY: Double =
    -50.0

  var score: Int =
    0

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setPaint(
          new GradientPaint(
            0,
            0,
            new Color(25,120,220),
            0,
            getHeight,
            new Color(170,235,255)
          )
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          Color.YELLOW
        )

        g.fillOval(
          ballX.toInt,
          ballY.toInt,
          35,
          35
        )

        g.setColor(
          new Color(150,85,20)
        )

        g.fillRoundRect(
          basketX.toInt,
          getHeight - 80,
          150,
          35,
          15,
          15
        )

        g.setColor(
          Color.WHITE
        )

        g.drawString(
          "SCORE: " + score,
          20,
          30
        )
      }
    }

  panel.addMouseMotionListener(
    new MouseMotionAdapter {

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

        basketX =
          math.max(
            0,
            math.min(
              panel.getWidth - 150,
              e.getX - 75
            )
          )
      }
    }
  )

  val catchTimer: Timer =
    new Timer(
      30,
      new ActionListener {

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

          ballY +=
            6

          if (
            ballY >
            panel.getHeight - 110
          ) {

            if (
              ballX + 35 >= basketX &&
              ballX <= basketX + 150
            ) {
              score +=
                1
            }

            ballX =
              RANDOM_GEN.nextDouble() *
              math.max(
                1,
                panel.getWidth - 35
              )

            ballY =
              -40
          }

          panel.repaint()
        }
      }
    )

  catchTimer.start()

  frame.addWindowListener(
    new java.awt.event.WindowAdapter {

      override def windowClosed(
        e: java.awt.event.WindowEvent
      ): Unit = {
        catchTimer.stop()
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )
}

// ============================================================
// GAME 10 - MAZE BALL
// ============================================================

def playMazeBall(): Unit = {

  val frame =
    gameFrame(
      "MAZE BALL - MOUSE"
    )

  var ballX: Double =
    70.0

  var ballY: Double =
    70.0

  val target =
    new java.awt.Rectangle(
      730,
      500,
      60,
      60
    )

  val walls =
    Array(
      new java.awt.Rectangle(
        170,
        90,
        520,
        20
      ),
      new java.awt.Rectangle(
        170,
        90,
        20,
        340
      ),
      new java.awt.Rectangle(
        420,
        220,
        20,
        330
      ),
      new java.awt.Rectangle(
        680,
        90,
        20,
        430
      )
    )

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setColor(
          Color.WHITE
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          new Color(75,75,85)
        )

        var i: Int =
          0

        while (
          i < walls.length
        ) {

          g.fillRect(
            walls(i).x,
            walls(i).y,
            walls(i).width,
            walls(i).height
          )

          i +=
            1
        }

        g.setColor(
          Color.GREEN
        )

        g.fillOval(
          target.x,
          target.y,
          target.width,
          target.height
        )

        g.setColor(
          Color.RED
        )

        g.fillOval(
          ballX.toInt,
          ballY.toInt,
          35,
          35
        )

        g.setColor(
          Color.BLACK
        )

        g.drawString(
          "DRAG RED BALL TO GREEN TARGET",
          20,
          30
        )
      }
    }

  panel.addMouseMotionListener(
    new MouseMotionAdapter {

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

        val nx =
          e.getX - 17

        val ny =
          e.getY - 17

        val probe =
          new java.awt.Rectangle(
            nx,
            ny,
            35,
            35
          )

        var hitWall: Boolean =
          false

        var i: Int =
          0

        while (
          i < walls.length
        ) {

          if (
            walls(i).intersects(
              probe
            )
          ) {
            hitWall =
              true
          }

          i +=
            1
        }

        if (!hitWall) {

          ballX =
            math.max(
              0,
              math.min(
                panel.getWidth - 35,
                nx
              )
            )

          ballY =
            math.max(
              0,
              math.min(
                panel.getHeight - 35,
                ny
              )
            )
        }

        if (
          target.contains(
            ballX.toInt + 17,
            ballY.toInt + 17
          )
        ) {

          JOptionPane.showMessageDialog(
            frame,
            "YOU WIN!"
          )
        }

        panel.repaint()
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )
}

// ============================================================
// GAME 11 - TIC TAC TOE
// ============================================================

def playTicTacToe(): Unit = {

  val frame =
    gameFrame(
      "TIC TAC TOE"
    )

  val board =
    Array.fill[Int](
      9
    )(
      0
    )

  var humanTurn: Boolean =
    true

  var finished: Boolean =
    false

  def win(
    player: Int
  ): Boolean = {

    val lines =
      Array(
        Array(0,1,2),
        Array(3,4,5),
        Array(6,7,8),
        Array(0,3,6),
        Array(1,4,7),
        Array(2,5,8),
        Array(0,4,8),
        Array(2,4,6)
      )

    var result: Boolean =
      false

    var i: Int =
      0

    while (
      i < lines.length
    ) {

      if (
        board(lines(i)(0)) == player &&
        board(lines(i)(1)) == player &&
        board(lines(i)(2)) == player
      ) {
        result =
          true
      }

      i +=
        1
    }

    result
  }

  def full(): Boolean = {

    var result: Boolean =
      true

    var i: Int =
      0

    while (
      i < 9
    ) {

      if (
        board(i) == 0
      ) {
        result =
          false
      }

      i +=
        1
    }

    result
  }

  def computerMove(): Int = {

    var chosen: Int =
      -1

    var i: Int =
      0

    while (
      i < 9 &&
      chosen < 0
    ) {

      if (
        board(i) == 0
      ) {
        chosen =
          i
      }

      i +=
        1
    }

    chosen
  }

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setColor(
          new Color(
            245,
            248,
            255
          )
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          Color.BLACK
        )

        g.setStroke(
          new BasicStroke(
            6
          )
        )

        var i: Int =
          1

        while (
          i < 3
        ) {

          g.drawLine(
            i * getWidth / 3,
            0,
            i * getWidth / 3,
            getHeight
          )

          g.drawLine(
            0,
            i * getHeight / 3,
            getWidth,
            i * getHeight / 3
          )

          i +=
            1
        }

        g.setFont(
          new Font(
            "Arial",
            Font.BOLD,
            82
          )
        )

        i =
          0

        while (
          i < 9
        ) {

          if (
            board(i) != 0
          ) {

            g.setColor(
              if (
                board(i) == 1
              )
                Color.BLUE
              else
                Color.RED
            )

            val col =
              i % 3

            val row =
              i / 3

            g.drawString(
              if (
                board(i) == 1
              )
                "X"
              else
                "O",
              col *
              getWidth / 3 +
              65,
              row *
              getHeight / 3 +
              105
            )
          }

          i +=
            1
        }

        g.setColor(
          Color.DARK_GRAY
        )

        g.setFont(
          new Font(
            "Arial",
            Font.BOLD,
            18
          )
        )

        g.drawString(
          if (finished)
            "GAME OVER"
          else if (humanTurn)
            "YOUR TURN"
          else
            "COMPUTER TURN",
          20,
          28
        )
      }
    }

  panel.addMouseListener(
    new MouseAdapter {

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

        if (
          humanTurn &&
          !finished
        ) {

          val cellW =
            math.max(
              1,
              panel.getWidth / 3
            )

          val cellH =
            math.max(
              1,
              panel.getHeight / 3
            )

          val col =
            math.max(
              0,
              math.min(
                2,
                e.getX / cellW
              )
            )

          val row =
            math.max(
              0,
              math.min(
                2,
                e.getY / cellH
              )
            )

          val idx =
            row * 3 + col

          if (
            board(idx) == 0
          ) {

            board(idx) =
              1

            if (
              win(1) ||
              full()
            ) {

              finished =
                true

            } else {

              humanTurn =
                false

              val ai =
                computerMove()

              if (
                ai >= 0
              ) {
                board(ai) =
                  2
              }

              if (
                win(2) ||
                full()
              ) {
                finished =
                  true
              } else {
                humanTurn =
                  true
              }
            }

            panel.repaint()
          }
        }
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )
}

// ============================================================
// GAME 12 - KEYBOARD RUNNER
// ============================================================

def playKeyboardRunner(): Unit = {

  val frame =
    gameFrame(
      "KEYBOARD RUNNER"
    )

  var px: Double =
    100.0

  var py: Double =
    430.0

  var velY: Double =
    0.0

  var score: Int =
    0

  var jumping: Boolean =
    false

  var running: Boolean =
    true

  val coins =
    ArrayBuffer[Point](
      new Point(260,390),
      new Point(470,320),
      new Point(670,390),
      new Point(820,280)
    )

  val enemies =
    ArrayBuffer[Point](
      new Point(530,450),
      new Point(760,450)
    )

  val panel =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[
            Graphics2D
          ]

        g.setPaint(
          new GradientPaint(
            0,
            0,
            new Color(70,180,250),
            0,
            getHeight,
            new Color(210,240,255)
          )
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        g.setColor(
          new Color(65,180,75)
        )

        g.fillRect(
          0,
          getHeight - 100,
          getWidth,
          100
        )

        g.setColor(
          Color.BLUE
        )

        g.fillRoundRect(
          px.toInt,
          py.toInt,
          45,
          65,
          10,
          10
        )

        g.setColor(
          Color.YELLOW
        )

        var i: Int =
          0

        while (
          i < coins.length
        ) {

          g.fillOval(
            coins(i).x,
            coins(i).y,
            25,
            25
          )

          i +=
            1
        }

        g.setColor(
          Color.RED
        )

        i =
          0

        while (
          i < enemies.length
        ) {

          g.fillRect(
            enemies(i).x,
            enemies(i).y,
            45,
            45
          )

          i +=
            1
        }

        g.setColor(
          Color.WHITE
        )

        g.drawString(
          "A/D or ARROWS   SPACE = JUMP   SCORE: " +
          score,
          15,
          28
        )

        if (!running) {

          g.setColor(
            Color.BLACK
          )

          g.fillRect(
            0,
            0,
            getWidth,
            getHeight
          )

          g.setColor(
            Color.WHITE
          )

          g.setFont(
            new Font(
              "Arial",
              Font.BOLD,
              42
            )
          )

          g.drawString(
            "GAME OVER",
            300,
            300
          )
        }
      }
    }

  panel.setFocusable(
    true
  )

  panel.addKeyListener(
    new KeyAdapter {

      override def keyPressed(
        e: KeyEvent
      ): Unit = {

        if (!running) {
          return
        }

        val k =
          e.getKeyCode

        if (
          k == KeyEvent.VK_LEFT ||
          k == KeyEvent.VK_A
        ) {

          px -=
            12

        } else if (
          k == KeyEvent.VK_RIGHT ||
          k == KeyEvent.VK_D
        ) {

          px +=
            12

        } else if (
          k == KeyEvent.VK_SPACE ||
          k == KeyEvent.VK_UP
        ) {

          if (!jumping) {

            velY =
              -11

            jumping =
              true
          }
        }

        px =
          math.max(
            0,
            math.min(
              panel.getWidth - 45,
              px
            )
          )
      }
    }
  )

  val runnerTimer: Timer =
    new Timer(
      30,
      new ActionListener {

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

          if (running) {

            velY +=
              0.55

            py +=
              velY

            val ground =
              panel.getHeight - 165

            if (
              py >= ground
            ) {

              py =
                ground

              velY =
                0

              jumping =
                false
            }

            var i: Int =
              coins.length - 1

            while (
              i >= 0
            ) {

              val c =
                coins(i)

              val got =
                px + 45 > c.x &&
                px < c.x + 25 &&
                py + 65 > c.y &&
                py < c.y + 25

              if (got) {

                coins.remove(i)

                score +=
                  10
              }

              i -=
                1
            }

            i =
              0

            while (
              i < enemies.length
            ) {

              enemies(i).x -=
                3

              if (
                enemies(i).x < -60
              ) {

                enemies(i).x =
                  panel.getWidth +
                  100 +
                  i * 200
              }

              val hit =
                px + 45 > enemies(i).x &&
                px < enemies(i).x + 45 &&
                py + 65 > enemies(i).y &&
                py < enemies(i).y + 45

              if (hit) {
                running =
                  false
              }

              i +=
                1
            }

            panel.repaint()
          }
        }
      }
    )

  runnerTimer.start()

  frame.addWindowListener(
    new java.awt.event.WindowAdapter {

      override def windowClosed(
        e: java.awt.event.WindowEvent
      ): Unit = {
        runnerTimer.stop()
      }
    }
  )

  frame.add(
    panel
  )

  frame.setVisible(
    true
  )

  panel.requestFocusInWindow()
}

// ============================================================
// GAME ROUTER
// ============================================================

def launchGame(
  name: String
): Unit = {

  if (
    name == "SLING BALL"
  ) {
    playSlingBall()

  } else if (
    name == "PONG"
  ) {
    playPong()

  } else if (
    name == "SNAKE"
  ) {
    playSnake()

  } else if (
    name == "BREAKOUT"
  ) {
    playBreakout()

  } else if (
    name == "FLAPPY BIRD"
  ) {
    playFlappy()

  } else if (
    name == "CAR DODGE"
  ) {
    playKeyboardCar()

  } else if (
    name == "SPACE SHOOTER"
  ) {
    playKeyboardSpace()

  } else if (
    name == "TARGET RUSH"
  ) {
    playTargetRush()

  } else if (
    name == "BALL CATCH"
  ) {
    playBallCatch()

  } else if (
    name == "MAZE BALL"
  ) {
    playMazeBall()

  } else if (
    name == "TIC TAC TOE"
  ) {
    playTicTacToe()

  } else {
    playKeyboardRunner()
  }
}

// ============================================================
// GAME STUDIO
// ============================================================

def openGameStudio(): Unit = {

  val frame =
    new JFrame(
      "ULTRA LEGEND GAME STUDIO"
    )

  frame.setDefaultCloseOperation(
    WindowConstants.DISPOSE_ON_CLOSE
  )

  frame.setSize(
    1250,
    780
  )

  frame.setLocationRelativeTo(
    mainFrame
  )

  val model =
    new DefaultListModel[String]()

  var i: Int =
    0

  while (
    i < gameDatabase.length
  ) {

    model.addElement(
      gameDatabase(i).name
    )

    i +=
      1
  }

  val list =
    new JList[String](
      model
    )

  list.setSelectionMode(
    ListSelectionModel.SINGLE_SELECTION
  )

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

  val search =
    new JTextField(
      14
    )

  val searchButton =
    button(
      "SEARCH"
    )

  val playButton =
    button(
      "PLAY"
    )

  val codeButton =
    button(
      "GAME CODE"
    )

  val closeButton =
    button(
      "CLOSE"
    )

  val info =
    new JTextArea()

  info.setEditable(
    false
  )

  info.setFont(
    new Font(
      "Monospaced",
      Font.PLAIN,
      13
    )
  )

  info.setText(
    "ULTRA LEGEND GAME STUDIO\n\n" +
    "MOUSE + KEYBOARD GAMES\n\n" +
    "DOUBLE CLICK A GAME TO PLAY.\n" +
    "SELECT GAME AND PRESS GAME CODE.\n"
  )

  val searchPanel =
    new JPanel(
      new FlowLayout(
        FlowLayout.LEFT
      )
    )

  searchPanel.add(
    new JLabel(
      "SEARCH:"
    )
  )

  searchPanel.add(
    search
  )

  searchPanel.add(
    searchButton
  )

  val left =
    new JPanel(
      new BorderLayout()
    )

  left.add(
    new JLabel(
      "GAME LIBRARY",
      SwingConstants.CENTER
    ),
    BorderLayout.NORTH
  )

  left.add(
    searchPanel,
    BorderLayout.CENTER
  )

  left.add(
    new JScrollPane(
      list
    ),
    BorderLayout.SOUTH
  )

  val right =
    new JPanel(
      new BorderLayout()
    )

  right.add(
    new JLabel(
      "GAME INFORMATION",
      SwingConstants.CENTER
    ),
    BorderLayout.NORTH
  )

  right.add(
    new JScrollPane(
      info
    ),
    BorderLayout.CENTER
  )

  val split =
    new JSplitPane(
      JSplitPane.HORIZONTAL_SPLIT,
      left,
      right
    )

  split.setDividerLocation(
    380
  )

  list.addListSelectionListener(
    new javax.swing.event.ListSelectionListener {

      override def valueChanged(
        e: javax.swing.event.ListSelectionEvent
      ): Unit = {

        if (
          !e.getValueIsAdjusting
        ) {

          val selected =
            list.getSelectedValue

          if (
            selected != null
          ) {

            var j: Int =
              0

            while (
              j < gameDatabase.length
            ) {

              if (
                gameDatabase(j).name ==
                selected
              ) {

                info.setText(
                  "GAME\n\n" +
                  gameDatabase(j).name +
                  "\n\nMODE: " +
                  gameDatabase(j).mode +
                  "\n\n" +
                  gameDatabase(j).description
                )
              }

              j +=
                1
            }
          }
        }
      }
    }
  )

  searchButton.addActionListener(
    new ActionListener {

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

        val q =
          search.getText
            .trim
            .toLowerCase

        model.clear()

        var j: Int =
          0

        while (
          j < gameDatabase.length
        ) {

          if (
            q.isEmpty ||
            gameDatabase(j).name
              .toLowerCase
              .indexOf(q) >= 0
          ) {

            model.addElement(
              gameDatabase(j).name
            )
          }

          j +=
            1
        }
      }
    }
  )

  playButton.addActionListener(
    new ActionListener {

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

        val selected =
          list.getSelectedValue

        if (
          selected != null
        ) {

          launchGame(
            selected
          )

        } else {

          JOptionPane.showMessageDialog(
            frame,
            "SELECT A GAME FIRST"
          )
        }
      }
    }
  )

  list.addMouseListener(
    new MouseAdapter {

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

        if (
          e.getClickCount == 2
        ) {

          val selected =
            list.getSelectedValue

          if (
            selected != null
          ) {

            launchGame(
              selected
            )
          }
        }
      }
    }
  )

  codeButton.addActionListener(
    new ActionListener {

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

        val selected =
          list.getSelectedValue

        if (
          selected != null
        ) {

          generatedAnimationCode =
            "// GAME SOURCE: " +
            selected +
            "\n\n" +
            "cleari()\n" +
            "setAnimationDelay(5)\n\n" +
            "// Launch from ULTRA LEGEND GAME STUDIO."

          val codeFrame =
            new JFrame(
              "GAME CODE"
            )

          codeFrame.setDefaultCloseOperation(
            WindowConstants.DISPOSE_ON_CLOSE
          )

          codeFrame.setSize(
            820,
            580
          )

          codeFrame.setLocationRelativeTo(
            frame
          )

          val area =
            new JTextArea(
              generatedAnimationCode
            )

          area.setFont(
            new Font(
              "Monospaced",
              Font.PLAIN,
              13
            )
          )

          val copyGame =
            button(
              "COPY"
            )

          copyGame.addActionListener(
            new ActionListener {

              override def actionPerformed(
                e: ActionEvent
              ): Unit = {
                copyText(
                  area.getText
                )
              }
            }
          )

          codeFrame.add(
            new JScrollPane(
              area
            ),
            BorderLayout.CENTER
          )

          codeFrame.add(
            copyGame,
            BorderLayout.SOUTH
          )

          codeFrame.setVisible(
            true
          )

        } else {

          JOptionPane.showMessageDialog(
            frame,
            "SELECT A GAME FIRST"
          )
        }
      }
    }
  )

  closeButton.addActionListener(
    new ActionListener {

      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        frame.dispose()
      }
    }
  )

  val bottom =
    new JPanel(
      new FlowLayout(
        FlowLayout.CENTER
      )
    )

  bottom.add(
    playButton
  )

  bottom.add(
    codeButton
  )

  bottom.add(
    closeButton
  )

  frame.setLayout(
    new BorderLayout()
  )

  frame.add(
    split,
    BorderLayout.CENTER
  )

  frame.add(
    bottom,
    BorderLayout.SOUTH
  )

  frame.setVisible(
    true
  )
}

// ============================================================
// MAIN CANVAS
// ============================================================

canvas =
  new JPanel {

    setFocusable(true)

    override def paintComponent(
      graphics: Graphics
    ): Unit = {

      super.paintComponent(
        graphics
      )

      val g =
        graphics.asInstanceOf[
          Graphics2D
        ]

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

      g.setColor(
        Color.WHITE
      )

      g.fillRect(
        0,
        0,
        getWidth,
        getHeight
      )

      if (importedImage != null) {
        val ix = (getWidth - importedImage.getWidth) / 2
        val iy = (getHeight - importedImage.getHeight) / 2
        g.drawImage(importedImage, ix, iy, null)
      }

      if (showGrid) {

        g.setColor(
          new Color(
            0,
            0,
            0,
            20
          )
        )

        var x: Int =
          0

        while (
          x <= CANVAS_W
        ) {

          g.drawLine(
            x,
            0,
            x,
            CANVAS_H
          )

          x +=
            50
        }

        var y: Int =
          0

        while (
          y <= CANVAS_H
        ) {

          g.drawLine(
            0,
            y,
            CANVAS_W,
            y
          )

          y +=
            50
        }
      }

      var i: Int =
        0

      while (
        i < drawItems.length
      ) {

        val item =
          drawItems(i)

        if (
          item.visible &&
          item.points.length >= 2
        ) {

          if (
            item.filled &&
            item.points.length >= 3
          ) {

            val path =
              new Path2D.Double()

            path.moveTo(
              item.points.head.x,
              item.points.head.y
            )

            var j: Int =
              1

            while (
              j < item.points.length
            ) {

              path.lineTo(
                item.points(j).x,
                item.points(j).y
              )

              j +=
                1
            }

            path.closePath()

            g.setColor(
              item.fill
            )

            g.fill(
              path
            )
          }

          g.setColor(
            item.stroke
          )

          g.setStroke(
            new BasicStroke(
              brushWidth,
              BasicStroke.CAP_ROUND,
              BasicStroke.JOIN_ROUND
            )
          )

          var j: Int =
            1

          while (
            j < item.points.length
          ) {

            g.drawLine(
              item.points(j - 1).x,
              item.points(j - 1).y,
              item.points(j).x,
              item.points(j).y
            )

            j +=
              1
          }
        }

        i +=
          1
      }

      if (
        tempPoints.length >= 2
      ) {

        g.setColor(
          strokeColor
        )

        g.setStroke(
          new BasicStroke(
            brushWidth,
            BasicStroke.CAP_ROUND,
            BasicStroke.JOIN_ROUND
          )
        )

        var j: Int =
          1

        while (
          j < tempPoints.length
        ) {

          g.drawLine(
            tempPoints(j - 1).x,
            tempPoints(j - 1).y,
            tempPoints(j).x,
            tempPoints(j).y
          )

          j +=
            1
        }
      }

      if (
        selectedIndex >= 0 &&
        selectedIndex < drawItems.length &&
        activeTool == "SELECT"
      ) {

        val r =
          boundsOf(
            drawItems(
              selectedIndex
            )
          )

        val box =
          new java.awt.Rectangle(
            r.x - 8,
            r.y - 8,
            r.width + 16,
            r.height + 16
          )

        g.setColor(
          new Color(
            0,
            105,
            240
          )
        )

        g.setStroke(
          new BasicStroke(
            2
          )
        )

        g.drawRect(
          box.x,
          box.y,
          box.width,
          box.height
        )

        val hs =
          handles(
            box
          )

        var h: Int =
          0

        while (
          h < hs.length
        ) {

          val hp =
            hs(h)._2

          g.setColor(
            Color.WHITE
          )

          g.fillRect(
            hp.x - 5,
            hp.y - 5,
            10,
            10
          )

          g.setColor(
            new Color(
              0,
              105,
              240
            )
          )

          g.drawRect(
            hp.x - 5,
            hp.y - 5,
            10,
            10
          )

          h +=
            1
        }
      }
    }
  }

canvas.setFocusable(
  true
)

// ============================================================
// CANVAS MOUSE INPUT
// ============================================================

canvas.addMouseListener(
  new MouseAdapter {

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

      val p =
        canvasPoint(e)

      if (
        activeTool == "SELECT"
      ) {

        val hit =
          findObject(p)

        if (
          selectedIndex >= 0 &&
          selectedIndex < drawItems.length
        ) {

          val handle =
            resizeHandle(
              drawItems(
                selectedIndex
              ),
              p
            )

          if (
            handle != ""
          ) {

            if (
              !drawItems(
                selectedIndex
              ).locked
            ) {

              saveHistory()

              resizingNow =
                true

              movingNow =
                false

              resizeHandle =
                handle

              originalPoints =
                drawItems(
                  selectedIndex
                ).points

              lastMousePoint =
                p
            }

          } else if (
            hit == selectedIndex
          ) {

            if (
              !drawItems(
                selectedIndex
              ).locked
            ) {

              saveHistory()

              movingNow =
                true

              resizingNow =
                false

              lastMousePoint =
                p
            }

          } else {

            selectedIndex =
              hit
          }

        } else {

          selectedIndex =
            hit
        }

        repaintCanvas()

      } else if (
        activeTool == "ERASER"
      ) {

        val hit =
          findObject(p)

        if (
          hit >= 0 &&
          !drawItems(hit).locked
        ) {

          saveHistory()

          drawItems.remove(
            hit
          )

          selectedIndex =
            -1

          repaintCanvas()

          setStatus(
            "ERASED"
          )
        }

      } else if (
        activeTool == "BUCKET"
      ) {

        val hit =
          findObject(p)

        if (
          hit >= 0
        ) {

          selectedIndex =
            hit

          fillSelected()

        } else {

          setStatus(
            "CLICK A CLOSED OBJECT"
          )
        }

      } else {

        saveHistory()

        tempPoints.clear()

        tempPoints +=
          p

        drawingNow =
          true

        selectedIndex =
          -1

        setStatus(
          "DRAWING..."
        )
      }
    }

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

      if (
        activeTool == "SELECT"
      ) {

        movingNow =
          false

        resizingNow =
          false

        repaintCanvas()

      } else if (
        drawingNow
      ) {

        tempPoints +=
          canvasPoint(e)

        if (
          tempPoints.length >= 2
        ) {

          drawItems +=
            DrawItem(
              tempPoints.toVector,
              strokeColor,
              fillColor,
              closed(
                tempPoints.toVector
              ),
              "FREEHAND",
              true,
              false
            )

          selectedIndex =
            drawItems.length - 1
        }

        tempPoints.clear()

        drawingNow =
          false

        repaintCanvas()

        setStatus(
          "DRAWING RECORDED"
        )
      }
    }
  }
)

// ============================================================
// CANVAS DRAG INPUT
// ============================================================

canvas.addMouseMotionListener(
  new MouseMotionAdapter {

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

      val p =
        canvasPoint(e)

      if (
        activeTool == "SELECT"
      ) {

        if (
          movingNow &&
          selectedIndex >= 0
        ) {

          moveSelected(
            p.x -
            lastMousePoint.x,
            p.y -
            lastMousePoint.y
          )

          lastMousePoint =
            p

          repaintCanvas()

        } else if (
          resizingNow &&
          selectedIndex >= 0
        ) {

          resizeSelected(
            p
          )

          repaintCanvas()
        }

      } else if (
        drawingNow
      ) {

        if (
          tempPoints.isEmpty ||
          distance(
            tempPoints.last,
            p
          ) >= 2.0
        ) {

          tempPoints +=
            p

          repaintCanvas()
        }
      }
    }
  }
)

canvas.setTransferHandler(imageDropHandler())

// Ctrl+V in Paint pastes an image directly from the system clipboard.
canvas.addKeyListener(
  new KeyAdapter {
    override def keyPressed(e: KeyEvent): Unit = {
      if (e.isControlDown && e.getKeyCode == KeyEvent.VK_V) {
        pasteImageFromClipboard()
      }
    }
  }
)

// ============================================================
// TOOLBAR BUTTONS
// ============================================================

val pencilBtn =
  button(
    "PENCIL"
  )

val selectBtn =
  button(
    "SELECT"
  )

val eraserBtn =
  button(
    "ERASER"
  )

val bucketBtn =
  button(
    "BUCKET"
  )

val undoBtn =
  button(
    "UNDO"
  )

val redoBtn =
  button(
    "REDO"
  )

val clearBtn =
  button(
    "CLEAR"
  )

val drawCodeBtn =
  button(
    "DRAW CODE"
  )

val animationBtn =
  button(
    "ANIMATION"
  )

val animationStudioBtn =
  button(
    "ANIMATION DRAW"
  )

val paint3DPreviewBtn =
  button(
    "3D PREVIEW"
  )

val copyBtn =
  button(
    "COPY"
  )

val saveBtn =
  button(
    "SAVE"
  )

val gamesBtn =
  button(
    "GAMES"
  )

val gridBtn =
  button(
    "GRID"
  )

val snapBtn =
  button(
    "SNAP"
  )

val strokeBtn =
  button(
    "STROKE"
  )

val fillBtn =
  button(
    "FILL"
  )

val importImageBtn =
  button(
    "IMPORT IMAGE"
  )

val pasteImageBtn =
  button(
    "PASTE IMAGE"
  )

val restoreMediaCodeBtn =
  button(
    "RESTORE MEDIA CODE"
  )

// ============================================================
// TOOLBAR
// ============================================================

val toolbar =
  new JPanel(
    new FlowLayout(
      FlowLayout.LEFT,
      3,
      3
    )
  )

toolbar.add(
  pencilBtn
)

toolbar.add(
  selectBtn
)

toolbar.add(
  eraserBtn
)

toolbar.add(
  bucketBtn
)

toolbar.add(
  undoBtn
)

toolbar.add(
  redoBtn
)

toolbar.add(
  clearBtn
)

toolbar.add(
  drawCodeBtn
)

toolbar.add(
  animationBtn
)

toolbar.add(
  paint3DPreviewBtn
)

toolbar.add(
  copyBtn
)

toolbar.add(
  saveBtn
)

toolbar.add(
  gamesBtn
)

toolbar.add(
  gridBtn
)

toolbar.add(
  snapBtn
)

toolbar.add(
  strokeBtn
)

toolbar.add(
  fillBtn
)

toolbar.add(
  importImageBtn
)

toolbar.add(
  pasteImageBtn
)

toolbar.add(
  restoreMediaCodeBtn
)

// ============================================================
// OBJECT / SHAPE LIST UI
// ============================================================

objectList =
  new JList[String](
    objectModel
  )

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

shapeList =
  new JList[String](
    shapeModel
  )

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

objectSearch =
  new JTextField(
    12
  )

val objectSearchBtn =
  button(
    "SEARCH"
  )

val objectSearchPanel =
  new JPanel(
    new FlowLayout(
      FlowLayout.LEFT
    )
  )

objectSearchPanel.add(
  objectSearch
)

objectSearchPanel.add(
  objectSearchBtn
)

val objectPanel =
  new JPanel(
    new BorderLayout()
  )

objectPanel.add(
  new JLabel(
    "OBJECT LIBRARY",
    SwingConstants.CENTER
  ),
  BorderLayout.NORTH
)

objectPanel.add(
  objectSearchPanel,
  BorderLayout.CENTER
)

objectPanel.add(
  new JScrollPane(
    objectList
  ),
  BorderLayout.SOUTH
)

val shapePanel =
  new JPanel(
    new BorderLayout()
  )

shapePanel.add(
  new JLabel(
    "SHAPE LIBRARY",
    SwingConstants.CENTER
  ),
  BorderLayout.NORTH
)

shapePanel.add(
  new JScrollPane(
    shapeList
  ),
  BorderLayout.CENTER
)

val libraryTabs =
  new JTabbedPane()

libraryTabs.addTab(
  "OBJECTS",
  objectPanel
)

libraryTabs.addTab(
  "SHAPES",
  shapePanel
)

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

objectSearchBtn.addActionListener(
  new ActionListener {

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

      val q =
        objectSearch
          .getText
          .trim
          .toLowerCase

      objectModel.clear()

      var i: Int =
        0

      while (
        i < objects.length
      ) {

        if (
          q.isEmpty ||
          objects(i)
            .toLowerCase
            .indexOf(q) >= 0
        ) {

          objectModel.addElement(
            objects(i)
          )
        }

        i +=
          1
      }
    }
  }
)

// ============================================================
// DOUBLE CLICK OBJECT
// ============================================================

objectList.addMouseListener(
  new MouseAdapter {

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

      if (
        e.getClickCount == 2
      ) {

        val selected =
          objectList.getSelectedValue

        if (
          selected != null
        ) {

          addObject(
            selected
          )
        }
      }
    }
  }
)

// ============================================================
// DOUBLE CLICK SHAPE
// ============================================================

shapeList.addMouseListener(
  new MouseAdapter {

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

      if (
        e.getClickCount == 2
      ) {

        val selected =
          shapeList.getSelectedValue

        if (
          selected != null
        ) {

          addShape(
            selected
          )
        }
      }
    }
  }
)

// ============================================================
// CODE AREA
// ============================================================

codeArea =
  new JTextArea()

codeArea.setEditable(
  false
)

codeArea.setFont(
  new Font(
    "Monospaced",
    Font.PLAIN,
    13
  )
)

codeArea.setText(
  "// DRAW SOMETHING\n" +
  "// THEN PRESS DRAW CODE"
)

// ============================================================
// PANELS
// ============================================================

val canvasPanel =
  new JPanel(
    new BorderLayout()
  )

canvasPanel.add(
  new JLabel(
    "DRAWING CANVAS",
    SwingConstants.CENTER
  ),
  BorderLayout.NORTH
)

canvasPanel.add(
  canvas,
  BorderLayout.CENTER
)

val codePanel =
  new JPanel(
    new BorderLayout()
  )

val codeHeader =
  new JPanel(
    new BorderLayout()
  )

codeHeader.add(
  new JLabel(
    "GENERATED KOJO / SCALA CODE",
    SwingConstants.CENTER
  ),
  BorderLayout.CENTER
)

val restoreMediaCodeB =
  button("RESTORE IMPORTED MEDIA CODE")

restoreMediaCodeB.addActionListener(
  new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      restoreImportedMediaCode()
    }
  }
)

codeHeader.add(
  restoreMediaCodeB,
  BorderLayout.SOUTH
)

codePanel.add(
  codeHeader,
  BorderLayout.NORTH
)

codePanel.add(
  new JScrollPane(
    codeArea
  ),
  BorderLayout.CENTER
)

val canvasCodeSplit =
  new JSplitPane(
    JSplitPane.HORIZONTAL_SPLIT,
    canvasPanel,
    codePanel
  )

canvasCodeSplit.setDividerLocation(
  660
)

val mainSplit =
  new JSplitPane(
    JSplitPane.HORIZONTAL_SPLIT,
    libraryTabs,
    canvasCodeSplit
  )

mainSplit.setDividerLocation(
  285
)

// ============================================================
// STATUS
// ============================================================

statusLabel =
  new JLabel(
    "ULTRA LEGEND X9 READY"
  )

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

// ============================================================
// MAIN FRAME
// ============================================================

mainFrame =
  new JFrame(
    "ULTRA LEGEND DRAW + GAME STUDIO X9"
  )

mainFrame.setDefaultCloseOperation(
  WindowConstants.DISPOSE_ON_CLOSE
)

mainFrame.setSize(
  1650,
  900
)

mainFrame.setMinimumSize(
  new Dimension(
    1100,
    650
  )
)

mainFrame.setLocationRelativeTo(
  null
)

mainFrame.setLayout(
  new BorderLayout(
    4,
    4
  )
)

mainFrame.add(
  toolbar,
  BorderLayout.NORTH
)

mainFrame.add(
  mainSplit,
  BorderLayout.CENTER
)

mainFrame.add(
  statusLabel,
  BorderLayout.SOUTH
)

// ============================================================
// TOOL EVENTS
// ============================================================

pencilBtn.addActionListener(
  new ActionListener {

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

      activeTool =
        "PENCIL"

      selectedIndex =
        -1

      canvas.setCursor(
        pencilCursor
      )

      setStatus(
        "PENCIL MODE"
      )

      canvas.requestFocusInWindow()
    }
  }
)

selectBtn.addActionListener(
  new ActionListener {

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

      activeTool =
        "SELECT"

      canvas.setCursor(
        Cursor.getPredefinedCursor(
          Cursor.DEFAULT_CURSOR
        )
      )

      setStatus(
        "SELECT / MOVE / RESIZE"
      )

      repaintCanvas()

      canvas.requestFocusInWindow()
    }
  }
)

eraserBtn.addActionListener(
  new ActionListener {

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

      activeTool =
        "ERASER"

      selectedIndex =
        -1

      canvas.setCursor(
        pencilCursor
      )

      setStatus(
        "ERASER MODE"
      )

      canvas.requestFocusInWindow()
    }
  }
)

bucketBtn.addActionListener(
  new ActionListener {

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

      activeTool =
        "BUCKET"

      canvas.setCursor(
        Cursor.getPredefinedCursor(
          Cursor.HAND_CURSOR
        )
      )

      setStatus(
        "BUCKET MODE"
      )

      canvas.requestFocusInWindow()
    }
  }
)

undoBtn.addActionListener(
  new ActionListener {

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

      undo()
    }
  }
)

redoBtn.addActionListener(
  new ActionListener {

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

      redo()
    }
  }
)

clearBtn.addActionListener(
  new ActionListener {

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

      clearAll()
    }
  }
)

importImageBtn.addActionListener(
  new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      chooseAndImportImage(mainFrame)
    }
  }
)

pasteImageBtn.addActionListener(
  new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      pasteImageFromClipboard()
    }
  }
)

restoreMediaCodeBtn.addActionListener(
  new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      restoreImportedMediaCode()
    }
  }
)

drawCodeBtn.addActionListener(
  new ActionListener {

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

      generateCode()
    }
  }
)

animationBtn.addActionListener(
  new ActionListener {

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

      // Open the independent Animation Studio window.
      // The regular ANIMATION button is intentionally a window launcher.
      openFrameAnimationStudio()
    }
  }
)

animationStudioBtn.addActionListener(
  new ActionListener {

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

      openFrameAnimationStudio()
    }
  }
)


paint3DPreviewBtn.addActionListener(
  new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      openPaint3DPreview()
    }
  }
)

copyBtn.addActionListener(
  new ActionListener {

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

      copyText(
        codeArea.getText
      )
    }
  }
)

saveBtn.addActionListener(
  new ActionListener {

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

      saveText(
        codeArea.getText
      )
    }
  }
)

gamesBtn.addActionListener(
  new ActionListener {

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

      openGameStudio()
    }
  }
)

gridBtn.addActionListener(
  new ActionListener {

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

      showGrid =
        !showGrid

      repaintCanvas()

      if (showGrid)
        setStatus("GRID ON")
      else
        setStatus("GRID OFF")
    }
  }
)

snapBtn.addActionListener(
  new ActionListener {

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

      snapToGrid =
        !snapToGrid

      if (snapToGrid)
        setStatus("SNAP ON")
      else
        setStatus("SNAP OFF")
    }
  }
)

strokeBtn.addActionListener(
  new ActionListener {

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

      val selected =
        JColorChooser.showDialog(
          mainFrame,
          "CHOOSE STROKE COLOR",
          strokeColor
        )

      if (
        selected != null
      ) {

        strokeColor =
          selected

        setStatus(
          "STROKE COLOR CHANGED"
        )
      }
    }
  }
)

fillBtn.addActionListener(
  new ActionListener {

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

      val selected =
        JColorChooser.showDialog(
          mainFrame,
          "CHOOSE FILL COLOR",
          fillColor
        )

      if (
        selected != null
      ) {

        fillColor =
          selected

        if (
          selectedIndex >= 0
        ) {
          fillSelected()
        } else {
          setStatus(
            "FILL COLOR CHANGED"
          )
        }
      }
    }
  }
)

// ============================================================
// KEYBOARD SHORTCUTS
// ============================================================

canvas.addKeyListener(
  new KeyAdapter {

    override def keyPressed(
      e: KeyEvent
    ): Unit = {

      val k: Int =
        e.getKeyCode

      if (
        e.isControlDown &&
        k == KeyEvent.VK_Z
      ) {

        undo()

      } else if (
        e.isControlDown &&
        k == KeyEvent.VK_Y
      ) {

        redo()

      } else if (
        e.isControlDown &&
        k == KeyEvent.VK_G
      ) {

        generateCode()

      } else if (
        e.isControlDown &&
        k == KeyEvent.VK_S
      ) {

        saveText(
          codeArea.getText
        )

      } else if (
        e.isControlDown &&
        k == KeyEvent.VK_C
      ) {

        copyText(
          codeArea.getText
        )

      } else if (
        e.isControlDown &&
        k == KeyEvent.VK_D
      ) {

        openGameStudio()

      } else if (
        k == KeyEvent.VK_DELETE
      ) {

        deleteSelected()

      } else if (
        k == KeyEvent.VK_P
      ) {

        activeTool =
          "PENCIL"

        setStatus(
          "PENCIL"
        )

      } else if (
        k == KeyEvent.VK_V
      ) {

        activeTool =
          "SELECT"

        setStatus(
          "SELECT"
        )

      } else if (
        k == KeyEvent.VK_B
      ) {

        activeTool =
          "BUCKET"

        setStatus(
          "BUCKET"
        )

      } else if (
        k == KeyEvent.VK_E
      ) {

        activeTool =
          "ERASER"

        setStatus(
          "ERASER"
        )

      } else if (
        k == KeyEvent.VK_F1
      ) {

        JOptionPane.showMessageDialog(
          mainFrame,
          "ULTRA LEGEND X9\n\n" +
          "P = Pencil\n" +
          "V = Select\n" +
          "B = Bucket\n" +
          "E = Eraser\n" +
          "DELETE = Delete\n\n" +
          "CTRL+Z = Undo\n" +
          "CTRL+Y = Redo\n" +
          "CTRL+G = Draw Code\n" +
          "CTRL+C = Copy\n" +
          "CTRL+S = Save\n" +
          "CTRL+D = Game Studio\n\n" +
          "SELECT + DRAG = MOVE\n" +
          "SELECT + HANDLE = RESIZE",
          "SHORTCUTS",
          JOptionPane.INFORMATION_MESSAGE
        )
      }
    }
  }
)

// ============================================================
// ============================================================
// SMOOTH PENCIL CURSOR + SIMPLE SOUND SUPPORT
// ============================================================

var selectedSoundFile: String = ""
var soundEnabled: Boolean = true


def makePencilCursor(): Cursor = {
  try {
    val img =
      new BufferedImage(
        32,
        32,
        BufferedImage.TYPE_INT_ARGB
      )

    val cg =
      img.createGraphics()

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

    cg.setColor(
      new Color(246, 205, 128)
    )

    val body =
      new java.awt.Polygon()

    body.addPoint(6, 4)
    body.addPoint(24, 22)
    body.addPoint(19, 27)
    body.addPoint(1, 9)

    cg.fillPolygon(body)

    cg.setColor(
      new Color(70, 45, 30)
    )

    cg.fillPolygon(
      Array(1, 9, 6, 4),
      Array(9, 28, 4, 4),
      4
    )

    cg.setColor(
      new Color(220, 65, 70)
    )

    cg.fillRect(
      20,
      18,
      7,
      7
    )

    cg.dispose()

    val toolkit =
      java.awt.Toolkit.getDefaultToolkit

    toolkit.createCustomCursor(
      img,
      new Point(2, 28),
      "pencil"
    )
  } catch {
    case _: Exception =>
      pencilCursor
  }
}

val pencilCursor =
  makePencilCursor()


def playStudioBeep(): Unit = {
  if (soundEnabled) {
    try {
      Toolkit.getDefaultToolkit.beep()
    } catch {
      case _: Exception =>
    }
  }
}

// ============================================================
// ULTRA INDEPENDENT FRAME ANIMATION STUDIO
// ============================================================

val animationCommandWords = Array(
  "NONE",
  "MOVE LEFT",
  "MOVE RIGHT",
  "MOVE UP",
  "MOVE DOWN",
  "JUMP",
  "BOUNCE",
  "WALK",
  "RUN",
  "FLOAT",
  "FALL",
  "RISE",
  "SHAKE",
  "WIGGLE",
  "WAVE",
  "DANCE",
  "SPIN",
  "ROTATE LEFT",
  "ROTATE RIGHT",
  "FLIP HORIZONTAL",
  "FLIP VERTICAL",
  "SCALE UP",
  "SCALE DOWN",
  "PULSE",
  "SWING",
  "ORBIT",
  "ORBIT REVERSE",
  "CIRCLE MOVE",
  "SPIRAL MOVE",
  "FIGURE EIGHT",
  "ZIGZAG",
  "JELLY",
  "HEARTBEAT",
  "TWINKLE",
  "SPARKLE",
  "VIBRATE",
  "TREMBLE",
  "TILT LEFT",
  "TILT RIGHT",
  "ROCK",
  "SPRING",
  "POP",
  "SLIDE LEFT",
  "SLIDE RIGHT",
  "SLIDE UP",
  "SLIDE DOWN",
  "FLY",
  "SOAR",
  "LAND",
  "TAKE OFF",
  "CAR DRIVE",
  "CAR REVERSE",
  "WHEEL SPIN",
  "BALL ROLL",
  "BALL BOUNCE",
  "CLOUD DRIFT",
  "RAIN FALL",
  "SNOW FALL",
  "FIRE FLICKER",
  "WATER WAVE",
  "TREE SWAY",
  "LEAF FALL",
  "BIRD FLY",
  "BUTTERFLY FLUTTER",
  "FISH SWIM",
  "CLOCK TICK",
  "CANDLE FLICKER",
  "LIGHT FLASH",
  "MAGIC APPEAR",
  "MAGIC DISAPPEAR",
  "TELEPORT",
  "PORTAL",
  "LASER",
  "EXPLODE",
  "PARTICLE BURST",
  "RING PULSE",
  "STAR SPIN",
  "MOON ORBIT",
  "PLANET ORBIT",
  "SUN PULSE",
  "ELECTRIC",
  "NEON PULSE",
  "GLITCH",
  "CYBER MOVE",
  "FUTURE DRIVE",
  "TIME WARP",
  "TIME REWIND",
  "SPEED UP",
  "SLOW MOTION",
  "EASE IN",
  "EASE OUT",
  "EASE IN OUT",
  "SPRING IN",
  "SPRING OUT",
  "ELASTIC",
  "RANDOM SHAKE",
  "RANDOM MOVE",
  "REVERSE",
  "PING PONG",
  "POSE",
  "STEP",
  "CHASE",
  "PATROL",
  "FOLLOW"
)


def animationClone(source: ArrayBuffer[DrawItem]): ArrayBuffer[DrawItem] = {
  cloneItems(source)
}


def animationSnapshot(): Unit = {
  ensureAnimationImageSlots()
  if (animationFrames.isEmpty) {
    animationFrames += animationClone(drawItems)
    animationImageFrames += null
  } else if (
    animationCurrentFrame >= 0 &&
    animationCurrentFrame < animationFrames.length
  ) {
    animationFrames(animationCurrentFrame) = animationClone(drawItems)
  }
}


def animationLoad(index: Int): Unit = {
  ensureAnimationImageSlots()
  if (animationFrames.nonEmpty) {
    val safe =
      math.max(
        0,
        math.min(
          animationFrames.length - 1,
          index
        )
      )

    drawItems.clear()
    drawItems ++= animationClone(animationFrames(safe))
    animationCurrentFrame = safe
    selectedIndex = -1
    repaintCanvas()
  }
}


def animationNextFrame(): Unit = {
  animationSnapshot()
  animationFrames += ArrayBuffer[DrawItem]()
  animationImageFrames += null
  animationCurrentFrame = animationFrames.length - 1
  drawItems.clear()
  selectedIndex = -1
}


def animationCopy(): Unit = {
  animationSnapshot()
  animationClipboard.clear()

  if (
    selectedIndex >= 0 &&
    selectedIndex < drawItems.length
  ) {
    animationClipboard +=
      animationClone(
        ArrayBuffer(drawItems(selectedIndex))
      ).head
  } else {
    animationClipboard ++=
      animationClone(drawItems)
  }
}


def animationPaste(): Unit = {
  if (animationClipboard.nonEmpty) {
    val pasted = animationClone(animationClipboard)

    val shifted =
      pasted.map { item =>
        item.copy(
          points =
            item.points.map { p =>
              new Point(
                p.x + 25,
                p.y + 15
              )
            }
        )
      }

    drawItems ++= shifted

    if (drawItems.nonEmpty) {
      selectedIndex = drawItems.length - 1
    }
  }
}


def animationTransform(
  source: ArrayBuffer[DrawItem],
  command: String,
  step: Int
): ArrayBuffer[DrawItem] = {

  val out =
    animationClone(source)

  var minX = 0.0
  var maxX = 0.0
  var minY = 0.0
  var maxY = 0.0
  var pointCount = 0

  var i = 0

  while (i < out.length) {
    var j = 0
    while (j < out(i).points.length) {
      val p = out(i).points(j)

      if (pointCount == 0) {
        minX = p.x.toDouble
        maxX = p.x.toDouble
        minY = p.y.toDouble
        maxY = p.y.toDouble
      } else {
        minX = math.min(minX, p.x.toDouble)
        maxX = math.max(maxX, p.x.toDouble)
        minY = math.min(minY, p.y.toDouble)
        maxY = math.max(maxY, p.y.toDouble)
      }

      pointCount += 1
      j += 1
    }
    i += 1
  }

  if (pointCount == 0) {
    return out
  }

  val cx =
    (minX + maxX) / 2.0

  val cy =
    (minY + maxY) / 2.0

  val t =
    step.toDouble

  val phase =
    t / 12.0

  i = 0

  while (i < out.length) {

    val item =
      out(i)

    val pts =
      item.points.map { p =>

        var nx = p.x.toDouble
        var ny = p.y.toDouble

        if (
          command == "MOVE LEFT" ||
          command == "SLIDE LEFT"
        ) {
          nx -= t * 5.0

        } else if (
          command == "MOVE RIGHT" ||
          command == "SLIDE RIGHT"
        ) {
          nx += t * 5.0

        } else if (
          command == "MOVE UP" ||
          command == "RISE" ||
          command == "SLIDE UP"
        ) {
          ny -= t * 4.0

        } else if (
          command == "MOVE DOWN" ||
          command == "FALL" ||
          command == "SLIDE DOWN"
        ) {
          ny += t * 4.0

        } else if (
          command == "JUMP" ||
          command == "BOUNCE" ||
          command == "SPRING"
        ) {
          ny -=
            math.sin(phase * math.Pi) * 60.0

        } else if (
          command == "WALK" ||
          command == "STEP"
        ) {
          nx +=
            math.sin(t * 0.55) * 10.0
          ny -=
            math.abs(math.sin(t * 0.55)) * 6.0

        } else if (command == "RUN") {
          nx += t * 7.0
          ny += math.sin(t * 0.9) * 7.0

        } else if (
          command == "FLOAT" ||
          command == "SOAR"
        ) {
          ny -= math.sin(t * 0.35) * 18.0

        } else if (
          command == "SCALE UP" ||
          command == "PULSE" ||
          command == "HEARTBEAT"
        ) {
          val k =
            1.0 + math.sin(phase * math.Pi) * 0.12

          nx =
            cx + (nx - cx) * k
          ny =
            cy + (ny - cy) * k

        } else if (command == "SCALE DOWN") {
          val k =
            1.0 - math.sin(phase * math.Pi) * 0.10

          nx =
            cx + (nx - cx) * k
          ny =
            cy + (ny - cy) * k

        } else if (
          command == "ROTATE LEFT" ||
          command == "SPIN" ||
          command == "STAR SPIN"
        ) {
          val a =
            -t * 7.0 * math.Pi / 180.0

          val dx =
            nx - cx

          val dy =
            ny - cy

          val rotatedX =
            dx * math.cos(a) - dy * math.sin(a)

          val rotatedY =
            dx * math.sin(a) + dy * math.cos(a)

          nx =
            cx + rotatedX

          ny =
            cy + rotatedY

        } else if (
          command == "ROTATE RIGHT" ||
          command == "TILT RIGHT"
        ) {
          val a =
            t * 7.0 * math.Pi / 180.0

          val dx =
            nx - cx

          val dy =
            ny - cy

          val rotatedX =
            dx * math.cos(a) - dy * math.sin(a)

          val rotatedY =
            dx * math.sin(a) + dy * math.cos(a)

          nx =
            cx + rotatedX

          ny =
            cy + rotatedY

        } else if (
          command == "FLIP HORIZONTAL"
        ) {
          nx =
            cx - (nx - cx)

        } else if (
          command == "FLIP VERTICAL"
        ) {
          ny =
            cy - (ny - cy)

        } else if (
          command == "WAVE" ||
          command == "WIGGLE" ||
          command == "TREE SWAY" ||
          command == "WATER WAVE"
        ) {
          ny +=
            math.sin(
              p.x.toDouble * 0.05 +
              t * 0.55
            ) * 12.0

        } else if (
          command == "SHAKE" ||
          command == "VIBRATE" ||
          command == "TREMBLE" ||
          command == "RANDOM SHAKE"
        ) {
          nx +=
            math.sin(t * 2.4) * 11.0
          ny +=
            math.cos(t * 2.0) * 8.0

        } else if (
          command == "SWING" ||
          command == "ROCK" ||
          command == "TILT LEFT"
        ) {
          val a =
            math.sin(phase * math.Pi) *
            10.0 * math.Pi / 180.0

          val dx =
            nx - cx

          val dy =
            ny - cy

          val rotatedX =
            dx * math.cos(a) - dy * math.sin(a)

          val rotatedY =
            dx * math.sin(a) + dy * math.cos(a)

          nx =
            cx + rotatedX

          ny =
            cy + rotatedY

        } else if (
          command == "ORBIT" ||
          command == "ORBIT REVERSE" ||
          command == "CIRCLE MOVE" ||
          command == "MOON ORBIT" ||
          command == "PLANET ORBIT"
        ) {
          var orbitSign = 1.0

          if (command == "ORBIT REVERSE") {
            orbitSign = -1.0
          }

          nx +=
            math.cos(t * 0.35 * orbitSign) * 16.0

          ny +=
            math.sin(t * 0.35 * orbitSign) * 10.0

        } else if (
          command == "SPIRAL MOVE"
        ) {
          val radius =
            math.min(60.0, t * 1.6)

          nx +=
            math.cos(t * 0.45) * radius

          ny +=
            math.sin(t * 0.45) * radius

        } else if (
          command == "FIGURE EIGHT"
        ) {
          nx +=
            math.sin(t * 0.35) * 20.0

          ny +=
            math.sin(t * 0.70) * 12.0

        } else if (
          command == "ZIGZAG"
        ) {
          var zig = -14.0

          if (step % 2 == 0) {
            zig = 14.0
          }

          nx += zig
          ny -= 4.0

        } else if (
          command == "TWINKLE" ||
          command == "SPARKLE"
        ) {
          val s =
            math.sin(t * 0.8)

          nx =
            cx + (nx - cx) * (1.0 + s * 0.05)

          ny =
            cy + (ny - cy) * (1.0 + s * 0.05)

        } else if (
          command == "POP" ||
          command == "SPRING IN"
        ) {
          val k =
            0.70 +
            math.sin(phase * math.Pi) * 0.35

          nx =
            cx + (nx - cx) * k

          ny =
            cy + (ny - cy) * k

        } else if (
          command == "SPRING OUT" ||
          command == "ELASTIC"
        ) {
          val k =
            1.0 +
            math.sin(phase * math.Pi * 2.0) * 0.16

          nx =
            cx + (nx - cx) * k

          ny =
            cy + (ny - cy) * k

        } else if (
          command == "JELLY"
        ) {
          nx +=
            math.sin(t * 0.8 + p.y * 0.03) * 8.0
          ny +=
            math.sin(t * 1.0 + p.x * 0.03) * 6.0

        } else if (
          command == "VIBRATE"
        ) {
          nx += math.sin(t * 4.0) * 5.0
          ny += math.cos(t * 4.0) * 5.0

        } else if (
          command == "DANCE" ||
          command == "CYBER MOVE"
        ) {
          nx +=
            math.sin(t * 0.60) * 14.0

          ny -=
            math.abs(math.sin(t * 0.60)) * 10.0

        } else if (
          command == "FLY" ||
          command == "TAKE OFF"
        ) {
          nx += t * 3.0
          ny -= t * 2.0

        } else if (
          command == "LAND"
        ) {
          ny += t * 2.0

        } else if (
          command == "CAR DRIVE" ||
          command == "FUTURE DRIVE"
        ) {
          nx += t * 8.0
          ny += math.sin(t * 0.5) * 2.0

        } else if (
          command == "CAR REVERSE"
        ) {
          nx -= t * 6.0

        } else if (
          command == "WHEEL SPIN" ||
          command == "BALL ROLL"
        ) {
          nx += t * 5.0
          ny += math.sin(t * 0.8) * 2.0

        } else if (
          command == "BALL BOUNCE"
        ) {
          ny -=
            math.abs(math.sin(t * 0.6)) * 45.0

        } else if (
          command == "CLOUD DRIFT"
        ) {
          nx += t * 3.0
          ny += math.sin(t * 0.3) * 5.0

        } else if (
          command == "RAIN FALL" ||
          command == "SNOW FALL" ||
          command == "LEAF FALL"
        ) {
          ny += t * 4.0
          nx += math.sin(t * 0.4) * 10.0

        } else if (
          command == "BIRD FLY" ||
          command == "BUTTERFLY FLUTTER"
        ) {
          nx += t * 4.0
          ny += math.sin(t * 0.7) * 20.0

        } else if (
          command == "FISH SWIM"
        ) {
          nx += t * 3.0
          ny += math.sin(t * 0.55) * 12.0

        } else if (
          command == "CLOCK TICK" ||
          command == "CANDLE FLICKER"
        ) {
          val a =
            math.sin(t * 0.9) * 3.0 * math.Pi / 180.0

          val dx =
            nx - cx

          val dy =
            ny - cy

          nx =
            cx + dx * math.cos(a) - dy * math.sin(a)

          ny =
            cy + dx * math.sin(a) + dy * math.cos(a)

        } else if (
          command == "PULSE" ||
          command == "RING PULSE" ||
          command == "SUN PULSE" ||
          command == "NEON PULSE"
        ) {
          val k =
            1.0 + math.sin(t * 0.7) * 0.08

          nx =
            cx + (nx - cx) * k

          ny =
            cy + (ny - cy) * k

        } else if (
          command == "EASE IN" ||
          command == "EASE IN OUT"
        ) {
          val e =
            (t / 12.0) * (t / 12.0)

          nx += e * 20.0

        } else if (
          command == "EASE OUT"
        ) {
          val q =
            t / 12.0

          val e =
            1.0 - (1.0 - q) * (1.0 - q)

          nx += e * 20.0

        } else if (
          command == "SPEED UP"
        ) {
          nx += t * t * 0.45

        } else if (
          command == "SLOW MOTION"
        ) {
          nx += t * 2.0

        } else if (
          command == "RANDOM MOVE"
        ) {
          nx +=
            math.sin(t * 1.7 + p.x * 0.01) * 8.0
          ny +=
            math.cos(t * 1.3 + p.y * 0.01) * 8.0

        } else if (
          command == "PING PONG"
        ) {
          var direction = 1.0

          if (step > 6) {
            direction = -1.0
          }

          nx +=
            direction * math.min(30.0, math.abs(t - 6.0) * 5.0)

        } else if (
          command == "REVERSE"
        ) {
          nx -= t * 4.0

        } else if (
          command == "TELEPORT"
        ) {
          if (step >= 6) {
            nx += 180.0
          }

        } else if (
          command == "MAGIC APPEAR" ||
          command == "MAGIC DISAPPEAR" ||
          command == "BLINK"
        ) {
          nx +=
            math.sin(t * 1.4) * 2.0

        } else if (
          command == "TIME WARP"
        ) {
          val q =
            math.sin(t * 0.5)

          nx += q * t
          ny -= q * 0.5 * t

        } else if (
          command == "TIME REWIND"
        ) {
          nx -= t * 3.0

        } else if (
          command == "POSE"
        ) {
          nx +=
            math.sin(t * 0.5) * 2.0

        } else if (
          command == "CHASE" ||
          command == "PATROL" ||
          command == "FOLLOW"
        ) {
          nx += t * 3.5
          ny += math.sin(t * 0.5) * 7.0
        }

        new Point(
          nx.toInt,
          ny.toInt
        )
      }.toVector

    out(i) =
      item.copy(
        points = pts
      )

    i += 1
  }

  out
}


// ============================================================
// DRAW ONE FRAME
// ============================================================

def paintFrameScene(
  g: Graphics2D,
  source: ArrayBuffer[DrawItem],
  ghost: ArrayBuffer[DrawItem],
  showGhost: Boolean,
  selected: Int
): Unit = {

  g.setColor(Color.WHITE)
  g.fillRect(
    0,
    0,
    CANVAS_W,
    CANVAS_H
  )

  g.setColor(
    new Color(235, 238, 245)
  )

  var gx = 0

  while (gx <= CANVAS_W) {
    g.drawLine(
      gx,
      0,
      gx,
      CANVAS_H
    )
    gx += 25
  }

  var gy = 0

  while (gy <= CANVAS_H) {
    g.drawLine(
      0,
      gy,
      CANVAS_W,
      gy
    )
    gy += 25
  }

  def drawItemsWithAlpha(
    items: ArrayBuffer[DrawItem],
    alpha: Int
  ): Unit = {

    var i = 0

    while (i < items.length) {

      val item =
        items(i)

      if (
        item.visible &&
        item.points.length >= 2
      ) {

        val oldComposite =
          g.getComposite

        g.setComposite(
          java.awt.AlphaComposite.getInstance(
            java.awt.AlphaComposite.SRC_OVER,
            alpha.toFloat / 255.0f
          )
        )

        if (
          item.filled &&
          item.points.length >= 3
        ) {
          g.setColor(item.fill)

          val xs =
            item.points.map(_.x).toArray

          val ys =
            item.points.map(_.y).toArray

          g.fillPolygon(
            xs,
            ys,
            xs.length
          )
        }

        g.setColor(item.stroke)

        g.setStroke(
          new BasicStroke(
            math.max(
              1.0f,
              brushWidth
            ),
            BasicStroke.CAP_ROUND,
            BasicStroke.JOIN_ROUND
          )
        )

        var j = 1

        while (j < item.points.length) {
          g.drawLine(
            item.points(j - 1).x,
            item.points(j - 1).y,
            item.points(j).x,
            item.points(j).y
          )
          j += 1
        }

        g.setComposite(oldComposite)
      }

      i += 1
    }
  }

  if (showGhost && ghost.nonEmpty) {
    drawItemsWithAlpha(
      ghost,
      55
    )
  }

  drawItemsWithAlpha(
    source,
    255
  )

  if (
    selected >= 0 &&
    selected < source.length
  ) {
    val r =
      boundsOf(
        source(selected)
      )

    g.setColor(
      new Color(
        30,
        120,
        255,
        190
      )
    )

    g.setStroke(
      new BasicStroke(
        2.0f,
        BasicStroke.CAP_ROUND,
        BasicStroke.JOIN_ROUND
      )
    )

    g.drawRect(
      r.x - 6,
      r.y - 6,
      r.width + 12,
      r.height + 12
    )

    val handlesNow =
      handles(r)

    var h = 0

    while (h < handlesNow.length) {
      val hp = handlesNow(h)._2
      g.fillRect(
        hp.x - 4,
        hp.y - 4,
        8,
        8
      )
      h += 1
    }
  }
}


// ============================================================
// GENERATED FRAME ANIMATION CODE
// ============================================================

def generatedAnimationItemsCode(
  items: ArrayBuffer[DrawItem],
  indent: String
): String = {

  val sb =
    new StringBuilder()

  var i = 0

  while (i < items.length) {

    val item =
      items(i)

    if (
      item.visible &&
      item.points.length >= 2
    ) {

      sb.append(
        indent +
        "// " +
        item.name.replace(
          "\"",
          "'"
        ) +
        "\n"
      )

      sb.append(
        indent +
        "val pts" +
        i +
        " = Array(\n"
      )

      var j = 0

      while (j < item.points.length) {

        sb.append(
          indent +
          "  new Point(" +
          item.points(j).x +
          "," +
          item.points(j).y +
          ")"
        )

        if (
          j < item.points.length - 1
        ) {
          sb.append(",")
        }

        sb.append("\n")
        j += 1
      }

      sb.append(
        indent +
        ")\n"
      )

      if (
        item.filled &&
        item.points.length >= 3
      ) {
        sb.append(
          indent +
          "g.setColor(new Color(" +
          item.fill.getRed +
          "," +
          item.fill.getGreen +
          "," +
          item.fill.getBlue +
          "))\n"
        )

        sb.append(
          indent +
          "val fx" +
          i +
          " = pts" +
          i +
          ".map(_.x).toArray\n"
        )

        sb.append(
          indent +
          "val fy" +
          i +
          " = pts" +
          i +
          ".map(_.y).toArray\n"
        )

        sb.append(
          indent +
          "g.fillPolygon(fx" +
          i +
          ", fy" +
          i +
          ", fx" +
          i +
          ".length)\n"
        )
      }

      sb.append(
        indent +
        "g.setColor(new Color(" +
        item.stroke.getRed +
        "," +
        item.stroke.getGreen +
        "," +
        item.stroke.getBlue +
        "))\n"
      )

      sb.append(
        indent +
        "g.setStroke(new BasicStroke(" +
        math.max(1.0f, brushWidth) +
        "f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))\n"
      )

      sb.append(
        indent +
        "var k" +
        i +
        " = 1\n"
      )

      sb.append(
        indent +
        "while (k" +
        i +
        " < pts" +
        i +
        ".length) {\n"
      )

      sb.append(
        indent +
        "  g.drawLine(pts" +
        i +
        "(k" +
        i +
        " - 1).x, pts" +
        i +
        "(k" +
        i +
        " - 1).y, pts" +
        i +
        "(k" +
        i +
        ").x, pts" +
        i +
        "(k" +
        i +
        ").y)\n"
      )

      sb.append(
        indent +
        "  k" +
        i +
        " += 1\n"
      )

      sb.append(
        indent +
        "}\n"
      )
    }

    i += 1
  }

  sb.toString
}


def generateFrameAnimationStudioCode(): Unit = {

  animationSnapshot()

  val sb =
    new StringBuilder()

  sb.append(
    "import java.awt.BasicStroke\n"
  )
  sb.append(
    "import java.awt.Color\n"
  )
  sb.append(
    "import java.awt.Graphics2D\n"
  )
  sb.append(
    "import java.awt.Point\n"
  )
  sb.append(
    "import java.awt.RenderingHints\n"
  )
  sb.append(
    "import java.awt.event.ActionEvent\n"
  )
  sb.append(
    "import javax.swing.JFrame\n"
  )
  sb.append(
    "import javax.swing.JPanel\n"
  )
  sb.append(
    "import javax.swing.Timer\n"
  )
  sb.append(
    "import javax.swing.WindowConstants\n"
  )
  sb.append(
    "import java.io.ByteArrayInputStream\n"
  )
  sb.append(
    "import java.util.Base64\n"
  )
  sb.append(
    "import javax.imageio.ImageIO\n"
  )
  sb.append(
    "import javax.sound.sampled.AudioSystem\n"
  )
  sb.append(
    "import javax.sound.sampled.Clip\n\n"
  )

  sb.append(
    "val W = " + CANVAS_W + "\n"
  )
  sb.append(
    "val H = " + CANVAS_H + "\n"
  )
  sb.append(
    "var frameNo = 0\n"
  )
  sb.append(
    "val voiceOverFile = \"\"\n"
  )
  sb.append(
    "def playVoice(): Unit = {\n"
  )
  sb.append(
    "  if (voiceOverFile != \"\") {\n"
  )
  sb.append(
    "    try {\n"
  )
  sb.append(
    "      val clip = AudioSystem.getClip\n"
  )
  sb.append(
    "      val ais = AudioSystem.getAudioInputStream(new java.io.File(voiceOverFile))\n"
  )
  sb.append(
    "      clip.open(ais)\n"
  )
  sb.append(
    "      clip.start()\n"
  )
  sb.append(
    "    } catch {\n"
  )
  sb.append(
    "      case _: Exception =>\n"
  )
  sb.append(
    "    }\n"
  )
  sb.append(
    "  }\n"
  )
  sb.append(
    "}\n\n"
  )

  sb.append(
    "val w = new JFrame(\"GENERATED ANIMATION\")\n"
  )
  sb.append(
    "w.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)\n"
  )
  sb.append(
    "w.setSize(1280, 820)\n"
  )
  sb.append(
    "w.setExtendedState(JFrame.MAXIMIZED_BOTH)\n"
  )
  sb.append(
    "w.setLocationRelativeTo(null)\n\n"
  )

  sb.append(
    "val panel = new JPanel {\n"
  )
  sb.append(
    "  override def paintComponent(graphics: java.awt.Graphics): Unit = {\n"
  )
  sb.append(
    "    super.paintComponent(graphics)\n"
  )
  sb.append(
    "    val g = graphics.asInstanceOf[Graphics2D]\n"
  )
  sb.append(
    "    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)\n"
  )
  sb.append(
    "    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)\n"
  )
  sb.append(
    "    val sx = getWidth.toDouble / W.toDouble\n"
  )
  sb.append(
    "    val sy = getHeight.toDouble / H.toDouble\n"
  )
  sb.append(
    "    val scale = math.max(0.1, math.min(sx, sy))\n"
  )
  sb.append(
    "    val drawW = W * scale\n"
  )
  sb.append(
    "    val drawH = H * scale\n"
  )
  sb.append(
    "    val ox = (getWidth - drawW) / 2.0\n"
  )
  sb.append(
    "    val oy = (getHeight - drawH) / 2.0\n"
  )
  sb.append(
    "    g.setColor(Color.BLACK)\n"
  )
  sb.append(
    "    g.fillRect(0, 0, getWidth, getHeight)\n"
  )
  sb.append(
    "    g.translate(ox, oy)\n"
  )
  sb.append(
    "    g.scale(scale, scale)\n"
  )
  sb.append(
    "    g.setColor(Color.WHITE)\n"
  )
  sb.append(
    "    g.fillRect(0, 0, W, H)\n"
  )

  var f = 0

  while (f < animationFrames.length) {

    sb.append(
      "    if (frameNo == " +
      f +
      ") {\n"
    )

    if (f < animationImageFrames.length && animationImageFrames(f) != null) {
      val encodedImage = java.util.Base64.getEncoder.encodeToString(imageToPngBytes(animationImageFrames(f)))
      sb.append("      val imageData" + f + " = Array(\n")
      var imageChunk = 0
      while (imageChunk < encodedImage.length) {
        val imageEnd = math.min(imageChunk + 120, encodedImage.length)
        sb.append("        \"" + encodedImage.substring(imageChunk, imageEnd) + "\"")
        if (imageEnd < encodedImage.length) sb.append(",")
        sb.append("\n")
        imageChunk = imageEnd
      }
      sb.append("      )\n")
      sb.append("      val img" + f + " = ImageIO.read(new ByteArrayInputStream(Base64.getDecoder.decode(imageData" + f + ".mkString)))\n")
      sb.append("      g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)\n")
      sb.append("      g.drawImage(img" + f + ", 0, 0, W, H, null)\n")
    }

    sb.append(
      generatedAnimationItemsCode(
        animationFrames(f),
        "      "
      )
    )

    sb.append(
      "    }\n"
    )

    f += 1
  }

  sb.append(
    "    g.setColor(Color.DARK_GRAY)\n"
  )
  sb.append(
    "    g.drawString(\"FRAME \" + (frameNo + 1) + \" / " + animationFrames.length + "\", 15, 20)\n"
  )
  sb.append(
    "  }\n"
  )
  sb.append(
    "}\n\n"
  )

  sb.append(
    "val timer = new Timer(" +
    math.max(
      20,
      1000 / animationFps
    ) +
    ", new javax.swing.AbstractAction {\n"
  )

  sb.append(
    "  override def actionPerformed(e: ActionEvent): Unit = {\n"
  )

  sb.append(
    "    frameNo += 1\n"
  )

  sb.append(
    "    if (frameNo >= " +
    animationFrames.length +
    ") {\n"
  )

  sb.append(
    "      frameNo = 0\n"
  )

  sb.append(
    "    }\n"
  )

  sb.append(
    "    panel.repaint()\n"
  )
  sb.append(
    "  }\n"
  )
  sb.append(
    "})\n"
  )

  sb.append(
    "w.add(panel)\n"
  )
  sb.append(
    "w.setVisible(true)\n"
  )
  sb.append(
    "timer.start()\n"
  )

  generatedAnimationCode =
    sb.toString

  if (codeArea != null) {
    codeArea.setText(
      generatedAnimationCode
    )
  }

  setStatus(
    "ANIMATION CODE GENERATED: " +
    animationFrames.length +
    " FRAMES"
  )
}


// ============================================================
// VIDEO -> FRAME ANIMATION IMPORT
// ============================================================

def addVideoFramesFromFile(video: java.io.File, targetMaxFrames: Int): Unit = {
  try {
    importedVideoPath = video.getAbsolutePath
    importedVideoName = video.getName

    val tempDir = Files.createTempDirectory("ultra_video_frames_").toFile
    val pattern = new java.io.File(tempDir, "frame_%05d.png")

    val command = Array(
      "ffmpeg", "-y", "-i", video.getAbsolutePath,
      "-vf", "fps=" + animationFps,
      "-frames:v", targetMaxFrames.toString,
      pattern.getAbsolutePath
    )

    val pb = new ProcessBuilder(command: _*)
    pb.redirectErrorStream(true)
    val process = pb.start()
    process.waitFor()

    val files = tempDir.listFiles()
    if (files == null || files.isEmpty) {
      setStatus("VIDEO IMPORT FAILED - FFMPEG PRODUCED NO FRAMES")
      return
    }

    val sorted = files.sortBy(_.getName)
    animationFrames.clear()
    animationImageFrames.clear()
    animationCurrentFrame = 0
    var fi = 0

    while (fi < sorted.length) {
      val img = javax.imageio.ImageIO.read(sorted(fi))
      if (img != null) {
        val frameItems = ArrayBuffer[DrawItem]()
        val fit = math.min(CANVAS_W.toDouble / img.getWidth.toDouble, CANVAS_H.toDouble / img.getHeight.toDouble)
        val dw = math.max(1, math.round(img.getWidth * fit).toInt)
        val dh = math.max(1, math.round(img.getHeight * fit).toInt)
        val copy = new BufferedImage(dw, dh, BufferedImage.TYPE_INT_ARGB)
        val cg = copy.createGraphics()
        cg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
        cg.drawImage(img, 0, 0, dw, dh, null)
        cg.dispose()
        // Store the frame as a special imported-image drawing item with a tiny marker path.
        // The actual frame pixels are kept in videoFrameImages for exact code generation.
        videoFrameImages += copy
      }
      fi += 1
    }

    if (videoFrameImages.nonEmpty) {
      var dummy = 0
      while (dummy < videoFrameImages.length) {
        animationFrames += ArrayBuffer[DrawItem]()
        dummy += 1
      }
      generateVideoAnimationCode()
      setStatus("VIDEO IMPORTED: " + sorted.length + " FRAMES @ " + animationFps + " FPS")
    }
  } catch {
    case ex: Throwable => setStatus("VIDEO IMPORT ERROR: " + ex.getMessage)
  }
}

var videoFrameImages =
  ArrayBuffer[BufferedImage]()

// Each animation frame may optionally contain one imported/pasted image.
// The image is rendered first, then normal vector drawings are drawn over it.
var animationImageFrames =
  ArrayBuffer[BufferedImage]()

def ensureAnimationImageSlots(): Unit = {
  while (animationImageFrames.length < animationFrames.length) {
    animationImageFrames += null
  }
  while (animationImageFrames.length > animationFrames.length) {
    animationImageFrames.remove(animationImageFrames.length - 1)
  }
}

def setAnimationImageFrame(img: BufferedImage): Unit = {
  if (img == null) return
  ensureAnimationImageSlots()
  if (animationFrames.isEmpty) {
    animationFrames += animationClone(drawItems)
    animationCurrentFrame = 0
  }
  ensureAnimationImageSlots()
  animationImageFrames(animationCurrentFrame) = img
  repaintCanvas()
}

def importImageToAnimationFrame(file: java.io.File): Unit = {
  try {
    val img = javax.imageio.ImageIO.read(file)
    if (img == null) {
      setStatus("ANIMATION IMAGE NOT SUPPORTED")
      return
    }
    val copy = new BufferedImage(img.getWidth, img.getHeight, BufferedImage.TYPE_INT_ARGB)
    val cg = copy.createGraphics()
    cg.drawImage(img, 0, 0, null)
    cg.dispose()
    setAnimationImageFrame(copy)
    importedImageOriginal = copy
    importedImage = fitImageToCanvas(copy)
    importedImagePath = file.getAbsolutePath
    importedImageName = file.getName
    generatedCode = generateExactImageCode(copy, file.getName)
    if (codeArea != null) codeArea.setText(generatedCode)
    setStatus("ANIMATION IMAGE IMPORTED: FRAME " + (animationCurrentFrame + 1) + " | " + copy.getWidth + "x" + copy.getHeight)
  } catch {
    case ex: Throwable => setStatus("ANIMATION IMAGE IMPORT ERROR: " + ex.getMessage)
  }
}

def defaultMediaDirectory(): java.io.File = {
  val home = new java.io.File(System.getProperty("user.home"))
  val downloads = new java.io.File(home, "Downloads")
  if (downloads.isDirectory) downloads else home
}

def chooseAndImportImageToAnimation(parent: JFrame): Unit = {
  val chooser = new JFileChooser(defaultMediaDirectory())
  chooser.setDialogTitle("IMPORT IMAGE INTO ANIMATION FRAME")
  chooser.setFileFilter(new FileNameExtensionFilter("Images: PNG, JPG, JPEG, GIF, BMP", "png", "jpg", "jpeg", "gif", "bmp"))
  if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
    importImageToAnimationFrame(chooser.getSelectedFile)
  }
}

def pasteImageToAnimationFrame(): Unit = {
  try {
    val cb = Toolkit.getDefaultToolkit.getSystemClipboard
    val t = cb.getContents(null)
    if (t != null && t.isDataFlavorSupported(DataFlavor.imageFlavor)) {
      val raw = t.getTransferData(DataFlavor.imageFlavor).asInstanceOf[java.awt.Image]
      val img = bufferedImageFromAwtImage(raw)
      if (img != null) {
        setAnimationImageFrame(img)
        importedImageOriginal = img
        importedImage = fitImageToCanvas(img)
        importedImagePath = "CLIPBOARD"
        importedImageName = "Animation Clipboard Image"
        generatedCode = generateExactImageCode(img, importedImageName)
        if (codeArea != null) codeArea.setText(generatedCode)
        setStatus("IMAGE PASTED INTO ANIMATION FRAME " + (animationCurrentFrame + 1))
        return
      }
    }
    setStatus("CLIPBOARD DOES NOT CONTAIN AN IMAGE")
  } catch {
    case ex: Throwable => setStatus("ANIMATION PASTE ERROR: " + ex.getMessage)
  }
}

def clearAnimationImageFrame(): Unit = {
  ensureAnimationImageSlots()
  if (animationFrames.nonEmpty && animationCurrentFrame >= 0 && animationCurrentFrame < animationImageFrames.length) {
    animationImageFrames(animationCurrentFrame) = null
    setStatus("IMAGE REMOVED FROM FRAME " + (animationCurrentFrame + 1))
  }
}

def animationImageDropHandler(): TransferHandler = {
  new TransferHandler() {
    override def canImport(support: TransferHandler.TransferSupport): Boolean = {
      support.isDataFlavorSupported(DataFlavor.imageFlavor) ||
      support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)
    }

    override def importData(support: TransferHandler.TransferSupport): Boolean = {
      try {
        if (!canImport(support)) return false
        val tr = support.getTransferable
        if (tr.isDataFlavorSupported(DataFlavor.imageFlavor)) {
          val raw = tr.getTransferData(DataFlavor.imageFlavor).asInstanceOf[java.awt.Image]
          val img = bufferedImageFromAwtImage(raw)
          if (img != null) {
            setAnimationImageFrame(img)
            importedImageOriginal = img
            importedImage = fitImageToCanvas(img)
            importedImagePath = "DRAGGED IMAGE"
            importedImageName = "Animation Dropped Image"
            generatedCode = generateExactImageCode(img, importedImageName)
            if (codeArea != null) codeArea.setText(generatedCode)
            setStatus("IMAGE DROPPED INTO ANIMATION FRAME " + (animationCurrentFrame + 1))
            true
          } else false
        } else {
          val data = tr.getTransferData(DataFlavor.javaFileListFlavor)
          val files = data.asInstanceOf[java.util.List[_]]
          if (files == null || files.isEmpty) return false
          val it = files.iterator()
          if (it.hasNext) {
            val f = it.next().asInstanceOf[java.io.File]
            if (isImageFile(f)) {
              importImageToAnimationFrame(f)
              true
            } else false
          } else false
        }
      } catch {
        case ex: Throwable =>
          setStatus("ANIMATION DROP ERROR: " + ex.getMessage)
          false
      }
    }
  }
}

def generateVideoAnimationCode(): Unit = {
  if (videoFrameImages.isEmpty) return

  val sb = new StringBuilder()
  sb.append("// ULTRA LEGEND - VIDEO TO FRAME ANIMATION CODE\n")
  sb.append("// Source: " + escapeScalaString(importedVideoName) + "\n")
  sb.append("// Frames are embedded as PNG Base64. Audio is not embedded.\n\n")
  sb.append("import java.awt.Graphics2D\n")
  sb.append("import java.awt.image.BufferedImage\n")
  sb.append("import java.io.ByteArrayInputStream\n")
  sb.append("import java.util.Base64\n")
  sb.append("import javax.imageio.ImageIO\n")
  sb.append("import javax.swing.JFrame\n")
  sb.append("import javax.swing.JPanel\n")
  sb.append("import javax.swing.Timer\n")
  sb.append("import javax.swing.WindowConstants\n\n")
  sb.append("val frames = new Array[BufferedImage](" + videoFrameImages.length + ")\n")

  var i = 0
  while (i < videoFrameImages.length) {
    val encoded = java.util.Base64.getEncoder.encodeToString(imageToPngBytes(videoFrameImages(i)))
    sb.append("val frameData" + i + " = Array(\n")
    var chunk = 0
    while (chunk < encoded.length) {
      val endChunk = math.min(chunk + 120, encoded.length)
      sb.append("  \"")
      sb.append(encoded.substring(chunk, endChunk))
      sb.append("\"")
      if (endChunk < encoded.length) sb.append(",")
      sb.append("\n")
      chunk = endChunk
    }
    sb.append(")\n")
    sb.append("val frame" + i + " = ImageIO.read(new ByteArrayInputStream(Base64.getDecoder.decode(frameData" + i + ".mkString)))\n")
    sb.append("frames(" + i + ") = frame" + i + "\n")
    i += 1
  }

  sb.append("val w = new JFrame(\"VIDEO FRAME ANIMATION\")\n")
  sb.append("w.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)\n")
  sb.append("w.setSize(" + CANVAS_W + ", " + (CANVAS_H + 35) + ")\n")
  sb.append("w.setLocationRelativeTo(null)\n")
  sb.append("var frameNo = 0\n")
  sb.append("val panel = new JPanel {\n")
  sb.append("  override def paintComponent(graphics: java.awt.Graphics): Unit = {\n")
  sb.append("    super.paintComponent(graphics)\n")
  sb.append("    val g = graphics.asInstanceOf[Graphics2D]\n")
  sb.append("    g.drawImage(frames(frameNo), 0, 0, null)\n")
  sb.append("    g.drawString(\"FRAME \" + (frameNo + 1) + \" / \" + frames.length, 12, 20)\n")
  sb.append("  }\n")
  sb.append("}\n")
  sb.append("w.add(panel)\n")
  sb.append("w.setVisible(true)\n")
  sb.append("val timer = new Timer(" + math.max(20, 1000 / animationFps) + ", new javax.swing.AbstractAction {\n")
  sb.append("  override def actionPerformed(e: java.awt.event.ActionEvent): Unit = {\n")
  sb.append("    frameNo += 1\n")
  sb.append("    if (frameNo >= frames.length) frameNo = 0\n")
  sb.append("    panel.repaint()\n")
  sb.append("  }\n")
  sb.append("})\n")
  sb.append("timer.start()\n")

  generatedAnimationCode = sb.toString
  if (codeArea != null) codeArea.setText(generatedAnimationCode)
}

// ============================================================
// OPEN INDEPENDENT ANIMATION WINDOW
// ============================================================

def openFrameAnimationStudio(): Unit = {

  animationFrames.clear()
  animationClipboard.clear()
  videoFrameImages.clear()
  animationImageFrames.clear()

  animationCurrentFrame = 0

  animationFrames +=
    animationClone(drawItems)
  animationImageFrames += null

  val win =
    new JFrame(
      "ULTRA ANIMATION DRAW STUDIO - FRAME BY FRAME"
    )

  win.setDefaultCloseOperation(
    WindowConstants.DISPOSE_ON_CLOSE
  )

  win.setSize(
    1500,
    980
  )

  win.setLocationRelativeTo(
    mainFrame
  )

  win.setLayout(
    new BorderLayout()
  )

  var drawing = false
  var moving = false
  var selecting = false

  var temp =
    ArrayBuffer[Point]()

  var pressPoint =
    new Point(0, 0)

  var originalSelection =
    Vector.empty[Point]

  var previewTimer: Timer =
    null

  var playing = false
  var previewIndex = 0

  var ghostMode = true
  var currentTool = "DRAW"
  var onionAlpha = 55
  var animationImageMode = "FIT"
  var animationImageZoom = 1.0

  val frameLabel =
    new JLabel(
      "FRAME 1 / 1",
      SwingConstants.CENTER
    )

  val modeLabel =
    new JLabel(
      "DRAW MODE"
    )

  val hintLabel =
    new JLabel(
      "DRAW: click-drag   SELECT: move   IMAGE: IMPORT / PASTE / CTRL+V / DROP   FULLSCREEN / ZOOM / FIT / FILL"
    )

  val editor =
    new JPanel {

      override def paintComponent(
        graphics: Graphics
      ): Unit = {

        super.paintComponent(
          graphics
        )

        val g =
          graphics.asInstanceOf[Graphics2D]

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

        g.setColor(
          new Color(
            218,
            222,
            232
          )
        )

        g.fillRect(
          0,
          0,
          getWidth,
          getHeight
        )

        val ox =
          80

        val oy =
          30

        val old =
          g.getTransform

        g.translate(
          ox,
          oy
        )

        ensureAnimationImageSlots()

        if (false) {
          val vi =
            if (playing) {
              math.max(
                0,
                math.min(
                  videoFrameImages.length - 1,
                  previewIndex
                )
              )
            } else {
              math.max(
                0,
                math.min(
                  videoFrameImages.length - 1,
                  animationCurrentFrame
                )
              )
            }

          val vimg =
            videoFrameImages(vi)

          val vx =
            (CANVAS_W - vimg.getWidth) / 2

          val vy =
            (CANVAS_H - vimg.getHeight) / 2

          g.drawImage(
            vimg,
            vx,
            vy,
            null
          )
        }

        if (animationFrames.nonEmpty && animationImageFrames.nonEmpty) {
          val ai =
            math.max(0, math.min(animationImageFrames.length - 1, if (playing) previewIndex else animationCurrentFrame))
          val aimg = animationImageFrames(ai)
          if (aimg != null) {
            // Large image preview: FIT preserves aspect ratio; FILL covers the whole page; ZOOM enlarges.
            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)
            val fitScale = math.min(CANVAS_W.toDouble / aimg.getWidth.toDouble, CANVAS_H.toDouble / aimg.getHeight.toDouble)
            val fillScale = math.max(CANVAS_W.toDouble / aimg.getWidth.toDouble, CANVAS_H.toDouble / aimg.getHeight.toDouble)
            val baseScale = if (animationImageMode == "FILL") fillScale else fitScale
            val finalScale = math.max(0.05, baseScale * animationImageZoom)
            val aw = math.max(1, math.round(aimg.getWidth * finalScale).toInt)
            val ah = math.max(1, math.round(aimg.getHeight * finalScale).toInt)
            val ix = (CANVAS_W - aw) / 2
            val iy = (CANVAS_H - ah) / 2
            g.drawImage(aimg, ix, iy, aw, ah, null)
          }
        }

        val activeSource =
          if (
            playing &&
            animationFrames.nonEmpty
          ) {
            animationFrames(
              math.max(
                0,
                math.min(
                  animationFrames.length - 1,
                  previewIndex
                )
              )
            )
          } else {
            drawItems
          }

        var ghostSource =
          ArrayBuffer[DrawItem]()

        if (
          ghostMode &&
          !playing &&
          animationCurrentFrame > 0
        ) {
          ghostSource =
            animationFrames(
              animationCurrentFrame - 1
            )
        }

        paintFrameScene(
            g,
            activeSource,
            ghostSource,
            ghostMode && !playing,
            if (playing) -1 else selectedIndex
          )

        if (onionAlpha > 0 && ghostMode && !playing) {
          modeLabel.setText(
            "DRAW MODE  |  ONION SKIN " +
            onionAlpha + "%"
          )
        }

        g.setTransform(old)
      }

      addMouseListener(
        new MouseAdapter {

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

            if (playing) {
              return
            }

            val p =
              new Point(
                math.max(
                  0,
                  math.min(
                    CANVAS_W - 1,
                    e.getX - 80
                  )
                ),
                math.max(
                  0,
                  math.min(
                    CANVAS_H - 1,
                    e.getY - 30
                  )
                )
              )

            if (
              p.x < 0 ||
              p.x >= CANVAS_W ||
              p.y < 0 ||
              p.y >= CANVAS_H
            ) {
              return
            }

            if (currentTool == "SELECT") {

              val hit =
                findObject(p)

              if (hit >= 0) {
                selectedIndex = hit
                moving = true
                selecting = true
                pressPoint = p
                originalSelection =
                  drawItems(hit).points
              } else {
                selectedIndex = -1
              }

            } else {

              drawing = true
              temp.clear()
              temp += p
              selectedIndex = -1
            }

            repaint()
          }

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

            if (playing) {
              return
            }

            if (moving && selecting) {

              moving = false
              selecting = false

              animationSnapshot()

              setStatus(
                "FRAME " +
                (animationCurrentFrame + 1) +
                " OBJECT MOVED"
              )
            }

            if (
              drawing &&
              temp.length >= 2
            ) {

              val pts =
                temp.toVector

              val item =
                DrawItem(
                  pts,
                  strokeColor,
                  fillColor,
                  closed(pts),
                  "FRAME DRAW",
                  true,
                  false
                )

              drawItems += item

              selectedIndex =
                drawItems.length - 1

              animationSnapshot()
            }

            temp.clear()
            drawing = false
            repaint()
          }
        }
      )

      addMouseMotionListener(
        new MouseMotionAdapter {

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

            if (playing) {
              return
            }

            val p =
              new Point(
                math.max(
                  0,
                  math.min(
                    CANVAS_W - 1,
                    e.getX - 80
                  )
                ),
                math.max(
                  0,
                  math.min(
                    CANVAS_H - 1,
                    e.getY - 30
                  )
                )
              )

            if (moving && selecting) {

              val dx =
                p.x - pressPoint.x

              val dy =
                p.y - pressPoint.y

              if (
                selectedIndex >= 0 &&
                selectedIndex < drawItems.length
              ) {

                val item =
                  drawItems(selectedIndex)

                drawItems(selectedIndex) =
                  item.copy(
                    points =
                      originalSelection.map { q =>
                        new Point(
                          q.x + dx,
                          q.y + dy
                        )
                      }
                  )
              }

            } else if (drawing) {
              if (temp.isEmpty || distance(temp.last, p) >= 2.0) {
                temp += p
              }
            }

            repaint()
          }
        }
      )
    }

  editor.setCursor(
    pencilCursor
  )

  editor.setTransferHandler(animationImageDropHandler())

  editor.setFocusable(true)
  editor.addKeyListener(
    new KeyAdapter {
      override def keyPressed(e: KeyEvent): Unit = {
        if (e.isControlDown && e.getKeyCode == KeyEvent.VK_V) {
          pasteImageToAnimationFrame()
          editor.repaint()
        }
      }
    }
  )

  // ----------------------------------------------------------
  // BUTTONS
  // ----------------------------------------------------------

  val drawB =
    button("DRAW")

  val selectB =
    button("SELECT / MOVE")

  val copyB =
    button("COPY DRAW")

  val pasteB =
    button("PASTE DRAW")

  val prevB =
    button("PREVIOUS")

  val nextB =
    button("NEXT FRAME")

  val clearB =
    button("CLEAR FRAME")

  val deleteB =
    button("DELETE SELECTED")

  val playB =
    button("PLAY")

  val stopB =
    button("STOP")

  val codeB =
    button("GENERATE ANIMATION CODE")

  val importImageAnimB =
    button("IMPORT IMAGE")

  val pasteImageAnimB =
    button("PASTE IMAGE")

  val clearImageAnimB =
    button("REMOVE IMAGE")

  val fitImageAnimB =
    button("FIT IMAGE")

  val fillImageAnimB =
    button("FILL IMAGE")

  val zoomImageAnimB =
    button("ZOOM IMAGE")

  val preview3dB =
    button("3D PREVIEW")

  val doneB =
    button("DONE")

  val strokeB =
    button("DRAW COLOR")

  val fillB =
    button("FILL COLOR")

  val fpsMinusB =
    button("FPS -")

  val fpsPlusB =
    button("FPS +")

  val onionB =
    button("ONION SKIN")

  val opacityMinusB =
    button("GHOST -")

  val opacityPlusB =
    button("GHOST +")

  val fpsLabel =
    new JLabel(
      "FPS: " + animationFps
    )

  val timeline =
    new JPanel(
      new FlowLayout(
        FlowLayout.LEFT,
        3,
        3
      )
    )

  val top =
    new JPanel(
      new FlowLayout(
        FlowLayout.LEFT,
        4,
        4
      )
    )

  top.add(frameLabel)
  top.add(drawB)
  top.add(selectB)
  top.add(copyB)
  top.add(pasteB)
  top.add(prevB)
  top.add(nextB)
  top.add(clearB)
  top.add(deleteB)
  top.add(playB)
  top.add(stopB)
  top.add(codeB)
  top.add(importImageAnimB)
  top.add(pasteImageAnimB)
  top.add(clearImageAnimB)
  top.add(fitImageAnimB)
  top.add(fillImageAnimB)
  top.add(zoomImageAnimB)
  top.add(doneB)
  top.add(preview3dB)
  top.add(strokeB)
  top.add(fillB)
  top.add(onionB)
  top.add(opacityMinusB)
  top.add(opacityPlusB)
  top.add(fpsMinusB)
  val soundB =
    button("SOUND ON/OFF")

  val chooseSoundB =
    button("CHOOSE WAV")

  val beepB =
    button("TEST SOUND")

  top.add(fpsPlusB)
  top.add(fpsLabel)
  top.add(soundB)
  top.add(chooseSoundB)
  top.add(beepB)

  val commandField =
    new JTextField(
      18
    )

  val commandCombo =
    new javax.swing.JComboBox[String](
      animationCommandWords
    )

  val autoB =
    button("AUTO ANIMATE")

  val addShapeB =
    button("ADD SHAPE")

  val addObjectB =
    button("ADD OBJECT")

  val shapeCombo =
    new javax.swing.JComboBox[String](
      shapes
    )

  val objectCombo =
    new javax.swing.JComboBox[String](
      objects
    )

  val commandPanel =
    new JPanel(
      new FlowLayout(
        FlowLayout.LEFT,
        4,
        4
      )
    )

  commandPanel.add(
    new JLabel(
      "ANIMATION:")
  )

  commandPanel.add(
    commandCombo
  )

  commandPanel.add(
    commandField
  )

  commandPanel.add(
    autoB
  )

  commandPanel.add(
    new JLabel("SHAPE")
  )

  commandPanel.add(
    shapeCombo
  )

  commandPanel.add(
    addShapeB
  )

  commandPanel.add(
    new JLabel("OBJECT")
  )

  commandPanel.add(
    objectCombo
  )

  commandPanel.add(
    addObjectB
  )

  commandPanel.add(
    hintLabel
  )

  addShapeB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        val selected =
          shapeCombo.getSelectedItem

        if (selected != null) {
          addShape(
            selected.toString
          )
          animationSnapshot()
          refreshAll()
        }
      }
    }
  )

  addObjectB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        val selected =
          objectCombo.getSelectedItem

        if (selected != null) {
          addObject(
            selected.toString
          )
          animationSnapshot()
          refreshAll()
        }
      }
    }
  )


  preview3dB.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = openPaint3DPreview()
  })

  // ----------------------------------------------------------
  // TIMELINE
  // ----------------------------------------------------------

  def rebuildTimeline(): Unit = {

    timeline.removeAll()

    var i = 0

    while (i < animationFrames.length) {

      val frameIndex = i

      val imageMarker =
        if (i < animationImageFrames.length && animationImageFrames(i) != null) " IMG" else ""

      val b =
        new JButton(
          "F" + (i + 1) + imageMarker
        )

      b.setFocusable(false)

      b.addActionListener(
        new ActionListener {

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

            animationSnapshot()
            animationLoad(frameIndex)
            refreshAll()
          }
        }
      )

      timeline.add(b)

      i += 1
    }

    timeline.revalidate()
    timeline.repaint()
  }

  def refreshAll(): Unit = {

    ensureAnimationImageSlots()

    frameLabel.setText(
      "FRAME " +
      (animationCurrentFrame + 1) +
      " / " +
      math.max(
        1,
        animationFrames.length
      )
    )

    fpsLabel.setText(
      "FPS: " +
      animationFps
    )

    rebuildTimeline()
    editor.repaint()
  }

  // ----------------------------------------------------------
  // MODE
  // ----------------------------------------------------------

  drawB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        currentTool = "DRAW"
        modeLabel.setText(
          "DRAW MODE"
        )
      }
    }
  )

  selectB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        currentTool = "SELECT"
        modeLabel.setText(
          "SELECT / MOVE MODE"
        )
      }
    }
  )

  // ----------------------------------------------------------
  // COPY / PASTE
  // ----------------------------------------------------------

  copyB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        animationCopy()
        playStudioBeep()
        modeLabel.setText(
          "COPIED " +
          animationClipboard.length +
          " DRAWINGS"
        )
      }
    }
  )

  pasteB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        animationPaste()
        playStudioBeep()
        animationSnapshot()
        currentTool = "SELECT"
        modeLabel.setText(
          "PASTED - SELECT / MOVE IT"
        )
        editor.repaint()
      }
    }
  )

  // ----------------------------------------------------------
  // NEXT / PREVIOUS
  // ----------------------------------------------------------

  nextB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        animationNextFrame()
        playStudioBeep()
        refreshAll()
        modeLabel.setText(
          "NEW FRAME " +
          (animationCurrentFrame + 1) +
          " - DRAW OR PASTE"
        )
      }
    }
  )

  prevB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        animationSnapshot()

        if (animationCurrentFrame > 0) {
          animationCurrentFrame -= 1
          animationLoad(
            animationCurrentFrame
          )
        }

        refreshAll()
      }
    }
  )

  clearB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        drawItems.clear()
        selectedIndex = -1
        animationSnapshot()
        editor.repaint()
      }
    }
  )

  deleteB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        deleteSelected()
        animationSnapshot()
        refreshAll()
      }
    }
  )

  // ----------------------------------------------------------
  // COLORS
  // ----------------------------------------------------------

  strokeB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        val c =
          JColorChooser.showDialog(
            win,
            "DRAW COLOR",
            strokeColor
          )

        if (c != null) {
          strokeColor = c
          modeLabel.setText(
            "DRAW COLOR RGB(" +
            c.getRed +
            "," +
            c.getGreen +
            "," +
            c.getBlue +
            ")"
          )
        }
      }
    }
  )

  fillB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {

        val c =
          JColorChooser.showDialog(
            win,
            "FILL COLOR",
            fillColor
          )

        if (c != null) {
          fillColor = c

          if (
            selectedIndex >= 0 &&
            selectedIndex < drawItems.length
          ) {
            fillSelected()
            animationSnapshot()
          }

          editor.repaint()
        }
      }
    }
  )

  // ----------------------------------------------------------
  // ONION SKIN
  // ----------------------------------------------------------

  onionB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        ghostMode = !ghostMode
        modeLabel.setText(
          if (ghostMode) {
            "ONION SKIN ON"
          } else {
            "ONION SKIN OFF"
          }
        )
        editor.repaint()
      }
    }
  )

  opacityMinusB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        onionAlpha =
          math.max(
            10,
            onionAlpha - 10
          )
        editor.repaint()
      }
    }
  )

  opacityPlusB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        onionAlpha =
          math.min(
            120,
            onionAlpha + 10
          )
        editor.repaint()
      }
    }
  )

  // ----------------------------------------------------------
  // FPS
  // ----------------------------------------------------------

  fpsMinusB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        animationFps =
          math.max(
            2,
            animationFps - 1
          )
        refreshAll()
      }
    }
  )

  fpsPlusB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        animationFps =
          math.min(
            30,
            animationFps + 1
          )
        refreshAll()
      }
    }
  )

  // ----------------------------------------------------------
  // PLAY
  // ----------------------------------------------------------

  playB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {

        animationSnapshot()

        if (animationFrames.length < 2) {
          modeLabel.setText(
            "MAKE 2 OR MORE FRAMES FIRST"
          )
          return
        }

        if (previewTimer != null) {
          previewTimer.stop()
        }

        playing = true
        previewIndex = 0

        previewTimer =
          new Timer(
            math.max(
              20,
              1000 / animationFps
            ),
            new ActionListener {
              override def actionPerformed(
                e: ActionEvent
              ): Unit = {

                previewIndex += 1

                if (
                  previewIndex >=
                  animationFrames.length
                ) {
                  previewIndex = 0
                }

                editor.repaint()
              }
            }
          )

        previewTimer.start()

        modeLabel.setText(
          "PLAYING ALL FRAMES"
        )
      }
    }
  )

  stopB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {

        if (previewTimer != null) {
          previewTimer.stop()
        }

        playing = false
        animationLoad(
          animationCurrentFrame
        )

        modeLabel.setText(
          "PLAY STOPPED"
        )

        editor.repaint()
      }
    }
  )

  // ----------------------------------------------------------
  // AUTO ANIMATE
  // ----------------------------------------------------------

  autoB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {

        val typed =
          commandField
            .getText
            .trim
            .toUpperCase

        val selected =
          commandCombo.getSelectedItem

        var command =
          "NONE"

        if (typed.nonEmpty) {
          command = typed
        } else if (selected != null) {
          command =
            selected.toString.toUpperCase
        }

        if (command != "NONE") {

          animationSnapshot()

          var base =
            animationClone(drawItems)

          var step = 1

          while (step <= 12) {

            val next =
              animationTransform(
                base,
                command,
                step
              )

            animationFrames +=
              animationClone(next)

            base = next
            step += 1
          }

          animationCurrentFrame =
            animationFrames.length - 1

          drawItems.clear()
          drawItems ++= animationClone(base)
          selectedIndex = -1

          refreshAll()

          modeLabel.setText(
            "AUTO ANIMATION: " +
            command +
            "  |  12 NEW FRAMES"
          )
        }
      }
    }
  )

  // ----------------------------------------------------------
  // SOUND
  // ----------------------------------------------------------

  soundB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        soundEnabled = !soundEnabled
        if (soundEnabled) {
          modeLabel.setText("SOUND ON")
        } else {
          modeLabel.setText("SOUND OFF")
        }
      }
    }
  )

  chooseSoundB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        val chooser =
          new JFileChooser()

        chooser.setDialogTitle(
          "CHOOSE WAV SOUND"
        )

        if (
          chooser.showOpenDialog(win) ==
          JFileChooser.APPROVE_OPTION
        ) {
          selectedSoundFile =
            chooser.getSelectedFile.getAbsolutePath

          modeLabel.setText(
            "SOUND: " +
            chooser.getSelectedFile.getName
          )

          playStudioBeep()
        }
      }
    }
  )

  beepB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {
        playStudioBeep()
        modeLabel.setText("TEST SOUND PLAYED")
      }
    }
  )

  // ----------------------------------------------------------
  // CODE / DONE
  // ----------------------------------------------------------

  codeB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {

        if (previewTimer != null) {
          previewTimer.stop()
        }

        playing = false

        generateFrameAnimationStudioCode()
        playStudioBeep()

        modeLabel.setText(
          "ANIMATION CODE READY - CHECK CODE PANEL"
        )
      }
    }
  )

  importImageAnimB.addActionListener(
    new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        chooseAndImportImageToAnimation(win)
        refreshAll()
        editor.requestFocusInWindow()
      }
    }
  )

  pasteImageAnimB.addActionListener(
    new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        pasteImageToAnimationFrame()
        refreshAll()
        editor.requestFocusInWindow()
      }
    }
  )

  clearImageAnimB.addActionListener(
    new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        clearAnimationImageFrame()
        editor.repaint()
      }
    }
  )

  fitImageAnimB.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      animationImageMode = "FIT"
      animationImageZoom = 1.0
      modeLabel.setText("IMAGE MODE: FIT")
      editor.repaint()
    }
  })

  fillImageAnimB.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      animationImageMode = "FILL"
      animationImageZoom = 1.0
      modeLabel.setText("IMAGE MODE: FILL FULL PAGE")
      editor.repaint()
    }
  })

  zoomImageAnimB.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      animationImageZoom = math.min(5.0, animationImageZoom + 0.25)
      modeLabel.setText("IMAGE ZOOM: " + animationImageZoom)
      editor.repaint()
    }
  })

  doneB.addActionListener(
    new ActionListener {
      override def actionPerformed(
        e: ActionEvent
      ): Unit = {

        if (previewTimer != null) {
          previewTimer.stop()
        }

        playing = false

        generateFrameAnimationStudioCode()

        modeLabel.setText(
          "DONE - FULL FRAME ANIMATION CODE GENERATED"
        )
      }
    }
  )

  // ----------------------------------------------------------
  // BOTTOM PANEL
  // ----------------------------------------------------------

  val bottom =
    new JPanel(
      new BorderLayout()
    )

  bottom.add(
    modeLabel,
    BorderLayout.NORTH
  )

  bottom.add(
    new JScrollPane(
      timeline
    ),
    BorderLayout.CENTER
  )

  bottom.add(
    commandPanel,
    BorderLayout.SOUTH
  )

  win.add(
    top,
    BorderLayout.NORTH
  )

  win.add(
    new JScrollPane(
      editor
    ),
    BorderLayout.CENTER
  )

  win.add(
    bottom,
    BorderLayout.SOUTH
  )

  win.addWindowListener(
    new java.awt.event.WindowAdapter {
      override def windowClosed(
        e: java.awt.event.WindowEvent
      ): Unit = {

        if (previewTimer != null) {
          previewTimer.stop()
        }

        animationStudioOpen =
          false
      }
    }
  )

  animationStudioOpen =
    true

  rebuildTimeline()
  refreshAll()
  win.setVisible(true)
}




// ============================================================
// INTERACTIVE 3D PAINT / ANIMATION PREVIEW
// Uses Java2D perspective projection for an orbitable 3D art board.
// The current Paint Studio drawing is projected onto the front face.
// ============================================================

def openPaint3DPreview(): Unit = {
  def capturePaintSnapshot(): BufferedImage = {
    val image = new BufferedImage(CANVAS_W, CANVAS_H, BufferedImage.TYPE_INT_ARGB)
    val g = image.createGraphics()
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
    g.setColor(Color.WHITE)
    g.fillRect(0, 0, CANVAS_W, CANVAS_H)
    if (importedImage != null) {
      val ix = (CANVAS_W - importedImage.getWidth) / 2
      val iy = (CANVAS_H - importedImage.getHeight) / 2
      g.drawImage(importedImage, ix, iy, null)
    }
    if (showGrid) {
      g.setColor(new Color(30, 45, 65, 22))
      var gx = 0
      while (gx <= CANVAS_W) { g.drawLine(gx, 0, gx, CANVAS_H); gx += 50 }
      var gy = 0
      while (gy <= CANVAS_H) { g.drawLine(0, gy, CANVAS_W, gy); gy += 50 }
    }
    drawItems.foreach { item =>
      if (item.visible && item.points.length >= 2) {
        val path = new Path2D.Double()
        path.moveTo(item.points.head.x, item.points.head.y)
        var pi = 1
        while (pi < item.points.length) { path.lineTo(item.points(pi).x, item.points(pi).y); pi += 1 }
        if (item.filled && item.points.length >= 3) {
          path.closePath()
          g.setColor(item.fill)
          g.fill(path)
        }
        g.setColor(item.stroke)
        g.setStroke(new BasicStroke(math.max(1.0f, brushWidth), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
        g.draw(path)
      }
    }
    g.dispose()
    image
  }

  var artImage = capturePaintSnapshot()
  var yaw = -0.32
  var pitch = -0.16
  var zoom = 1.0
  var autoRotate = true
  var dragX = -1
  var dragY = -1

  val window = new JFrame("PAINT + ANIMATION STUDIO | INTERACTIVE 3D PREVIEW")
  window.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
  window.setSize(1120, 760)
  window.setMinimumSize(new Dimension(820, 560))
  window.setLocationRelativeTo(mainFrame)
  window.setLayout(new BorderLayout(6, 6))
  window.getContentPane.setBackground(new Color(8, 12, 24))

  val help = new JLabel("  DRAG TO ORBIT   ?   MOUSE WHEEL TO ZOOM   ?   PREVIEW USES YOUR CURRENT PAINT / ANIMATION FRAME")
  help.setForeground(new Color(183, 210, 244))
  help.setFont(new Font("Arial", Font.BOLD, 11))

  val autoButton = new JButton("AUTO ROTATE: ON")
  val refreshButton = new JButton("REFRESH ART")
  val saveButton = new JButton("EXPORT PREVIEW PNG")
  val closeButton = new JButton("CLOSE")
  val controls = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 6))
  controls.setBackground(new Color(16, 23, 41))
  controls.add(autoButton)
  controls.add(refreshButton)
  controls.add(saveButton)
  controls.add(closeButton)
  controls.add(help)

  val preview = new JPanel {
    override protected def paintComponent(graphics: Graphics): Unit = {
      super.paintComponent(graphics)
      val g = graphics.create().asInstanceOf[Graphics2D]
      val w = getWidth
      val h = getHeight
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
      g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)
      g.setPaint(new GradientPaint(0f, 0f, new Color(5, 10, 27), 0f, h.toFloat, new Color(24, 20, 48)))
      g.fillRect(0, 0, w, h)

      // Ambient bloom and distant particles.
      var halo = 5
      while (halo >= 0) {
        val d = 150 + halo * 70
        g.setColor(new Color(38, 130, 255, 5 + (5 - halo) * 2))
        g.fillOval(w / 2 - d / 2, h / 2 - d / 2, d, d)
        halo -= 1
      }
      var star = 0
      while (star < 80) {
        val sx = ((star * 97 + 31) % math.max(1, w))
        val sy = ((star * 53 + 17) % math.max(1, h))
        val alpha = 25 + ((star * 13) % 80)
        g.setColor(new Color(118, 200, 255, alpha))
        val sz = if (star % 9 == 0) 3 else 1
        g.fillOval(sx, sy, sz, sz)
        star += 1
      }

      // Perspective grid below the floating board.
      val horizon = (h * 0.74).toInt
      g.setStroke(new BasicStroke(1.0f))
      var r = 0
      while (r < 10) {
        val f = (r + 1).toDouble / 10.0
        val yy = horizon + (math.pow(f, 1.7) * (h - horizon)).toInt
        g.setColor(new Color(43, 155, 238, (10 + f * 22).toInt))
        g.drawLine(w / 2 - (w * f * 0.48).toInt, yy, w / 2 + (w * f * 0.48).toInt, yy)
        r += 1
      }
      var col = -10
      while (col <= 10) {
        g.setColor(new Color(40, 131, 221, 28))
        g.drawLine(w / 2, horizon, w / 2 + col * w / 20, h)
        col += 1
      }

      case class Projected(x: Int, y: Int)
      def project(x: Double, y: Double, z: Double): Projected = {
        val cy = math.cos(yaw); val sy = math.sin(yaw)
        val cp = math.cos(pitch); val sp = math.sin(pitch)
        val rx = x * cy + z * sy
        val rz0 = -x * sy + z * cy
        val ry = y * cp - rz0 * sp
        val rz = y * sp + rz0 * cp
        val depth = math.max(2.8, 7.9 + rz)
        val focal = math.min(w, h).toDouble * 1.55 * zoom
        Projected((w / 2.0 + rx * focal / depth).toInt, (h / 2.0 - 8 + ry * focal / depth).toInt)
      }
      def polygon(points: Seq[Projected]): java.awt.Polygon = {
        val p = new java.awt.Polygon()
        points.foreach(q => p.addPoint(q.x, q.y))
        p
      }

      val thickness = 0.38
      val tl = project(-2.65, 1.72, 0.23)
      val tr = project( 2.65, 1.72, 0.23)
      val br = project( 2.65,-1.72, 0.23)
      val bl = project(-2.65,-1.72, 0.23)
      val btl = project(-2.65, 1.72, 0.23 - thickness)
      val btr = project( 2.65, 1.72, 0.23 - thickness)
      val bbr = project( 2.65,-1.72, 0.23 - thickness)
      val bbl = project(-2.65,-1.72, 0.23 - thickness)

      g.setColor(new Color(0, 0, 0, 95))
      g.fillOval(w / 2 - (w * 0.27).toInt, h / 2 + (h * 0.21).toInt, (w * 0.54).toInt, (h * 0.10).toInt)
      g.setColor(new Color(26, 35, 53))
      g.fillPolygon(polygon(Seq(btl, btr, bbr, bbl)))
      g.setPaint(new GradientPaint(btl.x.toFloat, btl.y.toFloat, new Color(76, 103, 140), bbl.x.toFloat, bbl.y.toFloat, new Color(16, 23, 39)))
      g.fillPolygon(polygon(Seq(tl, bl, bbl, btl)))
      g.setPaint(new GradientPaint(tr.x.toFloat, tr.y.toFloat, new Color(65, 89, 128), bbr.x.toFloat, bbr.y.toFloat, new Color(12, 18, 32)))
      g.fillPolygon(polygon(Seq(tr, br, bbr, btr)))
      g.setPaint(new GradientPaint(tl.x.toFloat, tl.y.toFloat, new Color(65, 90, 124), br.x.toFloat, br.y.toFloat, new Color(18, 26, 44)))
      g.fillPolygon(polygon(Seq(tl, tr, btr, btl)))
      g.fillPolygon(polygon(Seq(bl, br, bbr, bbl)))

      val frontPoly = polygon(Seq(tl, tr, br, bl))
      val oldClip = g.getClip
      g.setClip(frontPoly)
      val ax = (tr.x - tl.x).toDouble / CANVAS_W.toDouble
      val ay = (tr.y - tl.y).toDouble / CANVAS_W.toDouble
      val bx = (bl.x - tl.x).toDouble / CANVAS_H.toDouble
      val by = (bl.y - tl.y).toDouble / CANVAS_H.toDouble
      val tx = new java.awt.geom.AffineTransform(ax, ay, bx, by, tl.x.toDouble, tl.y.toDouble)
      g.drawImage(artImage, tx, null)
      g.setClip(oldClip)
      g.setColor(new Color(114, 213, 255, 220))
      g.setStroke(new BasicStroke(2.0f))
      g.drawPolygon(frontPoly)
      g.setColor(new Color(255, 255, 255, 90))
      g.drawLine(tl.x + 8, tl.y + 7, tr.x - 8, tr.y + 7)
      g.setFont(new Font("Arial", Font.BOLD, 12))
      g.setColor(new Color(210, 233, 255, 205))
      g.drawString("YTR 3D ART BOARD  ?  ORBIT VIEW", 18, h - 20)
      g.dispose()
    }
  }
  preview.setBackground(new Color(7, 12, 26))

  autoButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      autoRotate = !autoRotate
      autoButton.setText(if (autoRotate) "AUTO ROTATE: ON" else "AUTO ROTATE: OFF")
    }
  })
  refreshButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      artImage = capturePaintSnapshot()
      preview.repaint()
    }
  })
  saveButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      val chooser = new JFileChooser()
      chooser.setDialogTitle("Export 3D preview as PNG")
      chooser.setSelectedFile(new java.io.File("YTR_3D_Paint_Preview.png"))
      chooser.setFileFilter(new FileNameExtensionFilter("PNG image", "png"))
      if (chooser.showSaveDialog(window) == JFileChooser.APPROVE_OPTION) {
        try {
          var file = chooser.getSelectedFile
          if (!file.getName.toLowerCase.endsWith(".png")) file = new java.io.File(file.getParentFile, file.getName + ".png")
          val target = new BufferedImage(math.max(1, preview.getWidth), math.max(1, preview.getHeight), BufferedImage.TYPE_INT_ARGB)
          val tg = target.createGraphics()
          preview.paint(tg)
          tg.dispose()
          javax.imageio.ImageIO.write(target, "png", file)
          JOptionPane.showMessageDialog(window, "PNG exported successfully.\n" + file.getAbsolutePath, "Export complete", JOptionPane.INFORMATION_MESSAGE)
        } catch {
          case ex: Throwable => JOptionPane.showMessageDialog(window, "Could not export PNG: " + Option(ex.getMessage).getOrElse("unknown error"), "Export failed", JOptionPane.ERROR_MESSAGE)
        }
      }
    }
  })
  closeButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = window.dispose()
  })

  preview.addMouseListener(new MouseAdapter {
    override def mousePressed(e: MouseEvent): Unit = { dragX = e.getX; dragY = e.getY }
    override def mouseReleased(e: MouseEvent): Unit = { dragX = -1; dragY = -1 }
  })
  preview.addMouseMotionListener(new MouseMotionAdapter {
    override def mouseDragged(e: MouseEvent): Unit = {
      if (dragX >= 0 && dragY >= 0) {
        yaw += (e.getX - dragX) * 0.009
        pitch = math.max(-0.78, math.min(0.78, pitch + (e.getY - dragY) * 0.007))
        dragX = e.getX; dragY = e.getY
        preview.repaint()
      }
    }
  })
  preview.addMouseWheelListener(new java.awt.event.MouseWheelListener {
    override def mouseWheelMoved(e: java.awt.event.MouseWheelEvent): Unit = {
      zoom = math.max(0.55, math.min(1.55, zoom - e.getWheelRotation * 0.07))
      preview.repaint()
    }
  })

  val timer = new Timer(33, new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      if (autoRotate) { yaw += 0.0065; preview.repaint() }
    }
  })
  timer.setCoalesce(true)
  window.addWindowListener(new java.awt.event.WindowAdapter {
    override def windowClosed(e: java.awt.event.WindowEvent): Unit = timer.stop()
  })
  window.add(controls, BorderLayout.NORTH)
  window.add(preview, BorderLayout.CENTER)
  window.setVisible(true)
  timer.start()
}


// ============================================================
// ULTRA LAPTOP LAUNCHER
// REALISTIC LAPTOP SHELL + BRAND/COLOR + KEYBOARD + MOUSE
// ============================================================

def launchUltraLaptop(): Unit = {

  // ==========================================================
  // VIRTUAL LAPTOP / WINDOWS-STYLE DESKTOP STATE
  // ==========================================================

  var laptopColor = new Color(48, 52, 60)
  var laptopBrand = "ULTRA X9"
  var powered = false
  var desktopMode = "DESKTOP"
  var cursorX = 430
  var cursorY = 170
  var startOpen = false
  // Forward-declared because the desktop animation timer also paints the chassis.
  var base: JPanel = null

  // Animated desktop / boot sequence state.
  var wallpaperTime = 0.0
  var booting = false
  var bootProgress = 0.0
  var desktopAnimationEnabled = true
  val wallpaperParticles: Array[Array[Double]] = Array.fill(92) {
    Array(RANDOM_GEN.nextDouble(), RANDOM_GEN.nextDouble(),
      0.25 + RANDOM_GEN.nextDouble() * 0.95,
      RANDOM_GEN.nextDouble() * 6.28318,
      0.35 + RANDOM_GEN.nextDouble() * 1.25)
  }

  val keyMap =
    new scala.collection.mutable.HashMap[Int, JButton]()

  val laptopFrame =
    new JFrame("" + laptopBrand + " - ULTRA 3D WINDOWS LAPTOP")

  laptopFrame.setDefaultCloseOperation(
    WindowConstants.EXIT_ON_CLOSE
  )

  laptopFrame.setSize(1380, 940)
  laptopFrame.setMinimumSize(new Dimension(1120, 800))
  laptopFrame.setLocationRelativeTo(null)
  laptopFrame.setLayout(new BorderLayout(8, 8))

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

  val brandField =
    new JTextField("ULTRA X9", 14)

  val colorButton =
    button("LAPTOP COLOR")

  val powerButton =
    button("POWER ON")

  val resetButton =
    button("RESET")

  val topStatus =
    new JLabel("POWER OFF  |  Configure your laptop first")

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

  val topBar =
    new JPanel(new FlowLayout(FlowLayout.CENTER, 8, 6))

  topBar.setBackground(new Color(20, 23, 30))
  topBar.add(new JLabel("BRAND"))
  topBar.add(brandField)
  topBar.add(colorButton)
  topBar.add(powerButton)
  topBar.add(resetButton)
  topBar.add(topStatus)

  // ==========================================================
  // REAL-TIME 3D-STYLE WALLPAPER RENDERER
  // Animated particle field, perspective grid, orbit rings and
  // a rotating wireframe orb. This stays within Java2D/Scala Swing.
  // ==========================================================

  def drawRealisticWallpaper(g: Graphics2D, sx: Int, sy: Int, sw: Int, sh: Int): Unit = {
    val bottom = sy + sh - 52
    val horizonY = sy + (sh * 0.60).toInt
    val orbitX = sx + (sw * 0.79).toInt
    val orbitY = sy + (sh * 0.42).toInt
    val orbR = math.max(58, math.min(sw, sh).toInt / 6)

    g.setPaint(new GradientPaint(sx.toFloat, sy.toFloat,
      new Color(5, 11, 32), (sx + sw).toFloat, (sy + sh).toFloat,
      new Color(21, 26, 70)))
    g.fillRoundRect(sx, sy, sw, sh, 22, 22)

    // Deep nebula and soft atmospheric bloom.
    var glow = 5
    while (glow >= 0) {
      val d = (190 + glow * 86)
      g.setColor(new Color(31, 115, 206, 7 + (5 - glow) * 2))
      g.fillOval(sx + (sw * 0.64).toInt - d / 2, sy + (sh * 0.13).toInt - d / 2, d, d)
      g.setColor(new Color(133, 64, 220, 5 + (5 - glow)))
      g.fillOval(sx + (sw * 0.36).toInt - d / 3, sy + (sh * 0.40).toInt - d / 3, d * 2 / 3, d * 2 / 3)
      glow -= 1
    }

    // The perspective floor gives the wallpaper depth.
    g.setStroke(new BasicStroke(1.0f))
    var row = 0
    while (row < 13) {
      val f = (row + 1).toDouble / 13.0
      val y = horizonY + (math.pow(f, 1.8) * (bottom - horizonY)).toInt
      val half = (sw * (0.045 + f * 0.49)).toInt
      val alpha = (15 + f * 34).toInt
      g.setColor(new Color(51, 164, 235, alpha))
      g.drawLine(orbitX - half, y, orbitX + half, y)
      row += 1
    }
    var column = -13
    while (column <= 13) {
      val xBottom = orbitX + (column * sw * 0.043).toInt
      g.setColor(new Color(53, 158, 228, 28))
      g.drawLine(orbitX, horizonY, xBottom, bottom)
      column += 1
    }

    // Drifting light particles with faint comet tails.
    var pi = 0
    while (pi < wallpaperParticles.length) {
      val particle = wallpaperParticles(pi)
      particle(1) += particle(2) * 0.00042
      if (particle(1) > 0.94) {
        particle(1) = 0.13 + RANDOM_GEN.nextDouble() * 0.16
        particle(0) = RANDOM_GEN.nextDouble()
      }
      val wobble = math.sin(wallpaperTime * 0.7 + particle(3)) * 0.010
      val x = sx + ((particle(0) + wobble) * sw).toInt
      val y = sy + (particle(1) * sh).toInt
      val r = math.max(1, (particle(4) * (0.65 + (math.sin(wallpaperTime + particle(3)) + 1.0) * 0.45)).toInt)
      val alpha = math.max(20, math.min(155, (55 + 75 * (math.sin(wallpaperTime * 1.3 + particle(3)) + 1.0) / 2.0).toInt))
      g.setColor(new Color(104, 202, 255, math.max(8, alpha / 5)))
      g.drawLine(x, y + r * 4, x, y + r)
      g.setColor(new Color(153, 224, 255, alpha))
      g.fillOval(x - r, y - r, r * 2, r * 2)
      pi += 1
    }

    // Soft sphere glow.
    var halo = 5
    while (halo >= 0) {
      val radius = orbR + halo * 16
      g.setColor(new Color(37, 154, 255, 7 + (5 - halo) * 2))
      g.fillOval(orbitX - radius, orbitY - radius, radius * 2, radius * 2)
      halo -= 1
    }

    // Volumetric holographic core: layered shaded sphere + specular glints.
    // The latitude/longitude mesh drawn next gives the rotating globe its wireframe depth.
    g.setPaint(new GradientPaint(
      (orbitX - orbR).toFloat, (orbitY - orbR).toFloat, new Color(62, 180, 255, 225),
      (orbitX + orbR).toFloat, (orbitY + orbR).toFloat, new Color(3, 8, 31, 238)
    ))
    g.fillOval(orbitX - orbR, orbitY - orbR, orbR * 2, orbR * 2)
    var sphereGlow = 5
    while (sphereGlow >= 0) {
      val glowRadius = orbR - sphereGlow * math.max(1, orbR / 14)
      if (glowRadius > 0) {
        g.setColor(new Color(52, 170, 255, 5 + (5 - sphereGlow) * 3))
        g.fillOval(orbitX - glowRadius, orbitY - glowRadius, glowRadius * 2, glowRadius * 2)
      }
      sphereGlow -= 1
    }
    // Offset highlights imitate reflected light on a polished glass sphere.
    g.setColor(new Color(210, 248, 255, 52))
    g.fillOval(orbitX - (orbR * 0.54).toInt, orbitY - (orbR * 0.62).toInt,
      (orbR * 0.62).toInt, (orbR * 0.30).toInt)
    g.setColor(new Color(120, 219, 255, 32))
    g.fillOval(orbitX - (orbR * 0.68).toInt, orbitY - (orbR * 0.42).toInt,
      (orbR * 0.86).toInt, (orbR * 0.52).toInt)

    // Rotating 3D wireframe sphere. X/Y projection changes continuously.
    val rot = wallpaperTime * 0.44
    val tilt = 0.36 + math.sin(wallpaperTime * 0.31) * 0.09
    def spherePoint(lat: Double, lon: Double): (Int, Int, Double) = {
      val x0 = math.cos(lat) * math.cos(lon)
      val y0 = math.sin(lat)
      val z0 = math.cos(lat) * math.sin(lon)
      val rx = x0 * math.cos(rot) - z0 * math.sin(rot)
      val rz = x0 * math.sin(rot) + z0 * math.cos(rot)
      val ry = y0 * math.cos(tilt) - rz * math.sin(tilt)
      val depth = y0 * math.sin(tilt) + rz * math.cos(tilt)
      val perspective = 0.86 + depth * 0.13
      (orbitX + (rx * orbR * perspective).toInt,
       orbitY - (ry * orbR * 0.84 * perspective).toInt,
       depth)
    }
    g.setStroke(new BasicStroke(1.0f))
    var latIndex = -5
    while (latIndex <= 5) {
      val lat = latIndex * math.Pi / 12.0
      var seg = 0
      while (seg < 56) {
        val p1 = spherePoint(lat, seg * 2.0 * math.Pi / 56.0)
        val p2 = spherePoint(lat, (seg + 1) * 2.0 * math.Pi / 56.0)
        val alpha = math.max(24, math.min(170, (72 + ((p1._3 + p2._3) * 47)).toInt))
        g.setColor(new Color(67, 203, 255, alpha))
        g.drawLine(p1._1, p1._2, p2._1, p2._2)
        seg += 1
      }
      latIndex += 1
    }
    var lonIndex = 0
    while (lonIndex < 18) {
      val lon = lonIndex * 2.0 * math.Pi / 18.0
      var seg = 0
      while (seg < 34) {
        val lat1 = -math.Pi / 2.0 + seg * math.Pi / 34.0
        val lat2 = -math.Pi / 2.0 + (seg + 1) * math.Pi / 34.0
        val p1 = spherePoint(lat1, lon)
        val p2 = spherePoint(lat2, lon)
        val alpha = math.max(20, math.min(155, (58 + ((p1._3 + p2._3) * 43)).toInt))
        g.setColor(new Color(157, 123, 255, alpha))
        g.drawLine(p1._1, p1._2, p2._1, p2._2)
        seg += 1
      }
      lonIndex += 1
    }

    // Two rotating holographic rings around the globe.
    val savedTransform = g.getTransform
    g.setStroke(new BasicStroke(2.0f))
    g.setColor(new Color(86, 224, 255, 145))
    g.rotate(wallpaperTime * 0.19, orbitX, orbitY)
    g.drawOval(orbitX - orbR - 22, orbitY - orbR / 3, (orbR + 22) * 2, (orbR / 3) * 2)
    g.setTransform(savedTransform)
    g.setColor(new Color(183, 117, 255, 115))
    g.setStroke(new BasicStroke(1.4f))
    g.rotate(-wallpaperTime * 0.14, orbitX, orbitY)
    g.drawOval(orbitX - orbR - 35, orbitY - orbR / 2, (orbR + 35) * 2, orbR)
    g.setTransform(savedTransform)

    // A small orbiting spark makes the depth effect obvious.
    val sparkAngle = wallpaperTime * 1.15
    val sparkX = orbitX + (math.cos(sparkAngle) * orbR * 1.30).toInt
    val sparkY = orbitY + (math.sin(sparkAngle) * orbR * 0.43).toInt
    g.setColor(new Color(75, 207, 255, 50)); g.fillOval(sparkX - 11, sparkY - 11, 22, 22)
    g.setColor(new Color(211, 246, 255, 235)); g.fillOval(sparkX - 4, sparkY - 4, 8, 8)

    // Brand treatment and animated status lines.
    g.setFont(new Font("Arial", Font.BOLD, 46))
    g.setColor(new Color(35, 171, 255, 45))
    g.drawString("YTR", sx + 48, sy + 132)
    g.setColor(new Color(244, 250, 255))
    g.drawString("YTR", sx + 44, sy + 128)
    g.setFont(new Font("Arial", Font.BOLD, 15))
    g.setColor(new Color(119, 219, 255))
    g.drawString("ULTRA INTERACTIVE DESKTOP", sx + 47, sy + 156)
    g.setFont(new Font("Arial", Font.PLAIN, 12))
    g.setColor(new Color(206, 220, 248, 190))
    g.drawString("ANIMATED ENVIRONMENT  /  REAL-TIME EFFECTS", sx + 48, sy + 179)
    val pulse = (0.5 + 0.5 * math.sin(wallpaperTime * 2.0))
    g.setColor(new Color(57, 221, 177, (120 + pulse * 100).toInt))
    g.fillOval(sx + 50, sy + 198, 7, 7)
    g.setColor(new Color(218, 234, 255, 190))
    g.drawString("SYSTEM ONLINE  ?  GRAPHICS ENGINE ACTIVE", sx + 64, sy + 205)

    // Fine glass-like upper sheen.
    g.setPaint(new GradientPaint(sx, sy, new Color(255,255,255,24), sx, sy + (sh * 0.24).toInt, new Color(255,255,255,0)))
    g.fillRoundRect(sx, sy, sw, (sh * 0.28).toInt, 22, 22)
  }

  // ==========================================================
  // VIRTUAL SCREEN
  // ==========================================================

  val screen =
    new JPanel {
      setLayout(null)
      setFocusable(true)
      setBackground(Color.BLACK)

      override def paintComponent(graphics: Graphics): Unit = {
        super.paintComponent(graphics)

        val g = graphics.asInstanceOf[Graphics2D]
        g.setRenderingHint(
          RenderingHints.KEY_ANTIALIASING,
          RenderingHints.VALUE_ANTIALIAS_ON
        )

        val screenX = 24
        val screenY = 22
        val screenW = getWidth - 48
        val screenH = getHeight - 64

        g.setColor(new Color(10, 12, 17))
        g.fillRoundRect(0, 0, getWidth, getHeight, 30, 30)
        // Small webcam lens and indicator embedded in the top bezel.
        g.setColor(new Color(2, 5, 10))
        g.fillOval(getWidth / 2 - 6, 7, 12, 12)
        g.setColor(new Color(24, 83, 126))
        g.fillOval(getWidth / 2 - 3, 10, 6, 6)
        g.setColor(new Color(183, 228, 255, 160))
        g.fillOval(getWidth / 2 - 2, 10, 2, 2)

        if (!powered) {
          g.setColor(new Color(24, 28, 38))
          g.fillRoundRect(screenX, screenY, screenW, screenH, 22, 22)

          g.setColor(Color.WHITE)
          g.setFont(new Font("Arial", Font.BOLD, 42))
          val title = laptopBrand
          g.drawString(title, getWidth / 2 - 160, getHeight / 2 - 36)

          g.setFont(new Font("Arial", Font.PLAIN, 18))
          g.setColor(new Color(190, 198, 215))
          g.drawString(
            "Press POWER ON to start your Windows-style laptop",
            getWidth / 2 - 250,
            getHeight / 2 + 8
          )

          g.setColor(laptopColor.brighter())
          g.setFont(new Font("Arial", Font.BOLD, 16))
          g.drawString("BRAND: " + laptopBrand, getWidth / 2 - 95, getHeight / 2 + 50)

        } else if (booting) {
          // Use integer coordinates here; Java's GradientPaint constructor accepts
          // these via primitive numeric widening. This avoids the old Kojo parser
          // error where a Float result was accidentally treated as a function.
          g.setPaint(new GradientPaint(0f, screenY, new Color(4, 9, 25),
            getWidth, screenY + screenH, new Color(14, 43, 80)))
          g.fillRoundRect(screenX, screenY, screenW, screenH, 22, 22)
          val cx = getWidth / 2
          val cy = getHeight / 2 - 25
          g.setColor(new Color(66, 192, 255, 30))
          g.fillOval(cx - 92, cy - 92, 184, 184)
          g.setStroke(new BasicStroke(3.0f))
          g.setColor(new Color(64, 207, 255, 210))
          g.drawArc(cx - 68, cy - 68, 136, 136, (wallpaperTime * 90).toInt % 360, 245)
          g.setStroke(new BasicStroke(1.3f))
          g.setColor(new Color(173, 135, 255, 155))
          g.drawOval(cx - 80, cy - 80, 160, 160)
          g.setFont(new Font("Arial", Font.BOLD, 48))
          g.setColor(Color.WHITE)
          val bootTitle = laptopBrand
          g.drawString(bootTitle, cx - g.getFontMetrics.stringWidth(bootTitle) / 2, cy + 5)
          g.setFont(new Font("Arial", Font.PLAIN, 14))
          g.setColor(new Color(177, 213, 242))
          val bootText = "STARTING ULTRA EXPERIENCE"
          g.drawString(bootText, cx - g.getFontMetrics.stringWidth(bootText) / 2, cy + 38)
          val barW = 300
          val barX = cx - barW / 2
          val barY = cy + 74
          g.setColor(new Color(255,255,255,35)); g.fillRoundRect(barX, barY, barW, 8, 8, 8)
          g.setPaint(new GradientPaint(barX, barY, new Color(63, 208, 255), barX + barW, barY + 8, new Color(144, 106, 255)))
          g.fillRoundRect(barX, barY, (barW * bootProgress).toInt, 8, 8, 8)
          g.setFont(new Font("Arial", Font.PLAIN, 11))
          g.setColor(new Color(171, 194, 219))
          g.drawString("INITIALIZING GRAPHICS  ?  LOADING DESKTOP", cx - 150, barY + 28)
        } else {
          drawRealisticWallpaper(g, screenX, screenY, screenW, screenH)

          // Desktop title
          g.setColor(Color.WHITE)
          g.setFont(new Font("Arial", Font.BOLD, 21))
          g.drawString(laptopBrand, screenX + 24, screenY + 34)

          g.setFont(new Font("Arial", Font.PLAIN, 12))
          g.drawString(
            "ULTRA WINDOWS DESKTOP",
            screenX + 24,
            screenY + 52
          )

          // Taskbar
          val taskY = screenY + screenH - 48
          g.setColor(new Color(6, 12, 27, 210))
          g.fillRoundRect(screenX + 5, taskY, screenW - 10, 42, 16, 16)
          g.setColor(new Color(126, 196, 255, 92))
          g.drawRoundRect(screenX + 5, taskY, screenW - 10, 42, 16, 16)

          // Glass taskbar controls: four-pane launcher, search and task view.
          g.setColor(new Color(45, 157, 255))
          g.fillRoundRect(screenX + 17, taskY + 10, 9, 9, 2, 2)
          g.fillRoundRect(screenX + 28, taskY + 10, 9, 9, 2, 2)
          g.fillRoundRect(screenX + 17, taskY + 21, 9, 9, 2, 2)
          g.fillRoundRect(screenX + 28, taskY + 21, 9, 9, 2, 2)
          g.setColor(Color.WHITE)
          g.setFont(new Font("Arial", Font.BOLD, 12))
          g.drawString("START", screenX + 43, taskY + 26)
          g.setColor(new Color(213, 226, 247, 205))
          g.setStroke(new BasicStroke(1.7f))
          g.drawOval(screenX + 93, taskY + 12, 12, 12)
          g.drawLine(screenX + 103, taskY + 22, screenX + 109, taskY + 28)
          g.drawRoundRect(screenX + 130, taskY + 11, 16, 17, 3, 3)
          g.drawLine(screenX + 133, taskY + 16, screenX + 143, taskY + 16)

          g.setFont(new Font("Arial", Font.PLAIN, 12))
          g.drawString(
            desktopMode,
            screenX + 155,
            taskY + 26
          )

          // Clock
          val now = new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date())
          g.drawString(now, screenX + screenW - 84, taskY + 25)

          // Custom cursor
          val cx = math.max(screenX + 3, math.min(screenX + screenW - 25, cursorX))
          val cy = math.max(screenY + 3, math.min(screenY + screenH - 40, cursorY))

          val arrow = new Path2D.Double()
          arrow.moveTo(cx, cy)
          arrow.lineTo(cx + 2, cy + 27)
          arrow.lineTo(cx + 10, cy + 20)
          arrow.lineTo(cx + 19, cy + 33)
          arrow.lineTo(cx + 25, cy + 29)
          arrow.lineTo(cx + 16, cy + 17)
          arrow.lineTo(cx + 27, cy + 17)
          arrow.closePath()

          g.setColor(Color.BLACK)
          g.fill(arrow)
          g.translate(1, 1)
          g.setColor(Color.WHITE)
          g.fill(arrow)
          g.translate(-1, -1)
        }
      }
    }

  // ==========================================================
  // DESKTOP APP HELPER
    // ==========================================================

  def desktopTileIcon(title: String): ImageIcon = {
    val img = new BufferedImage(44, 44, BufferedImage.TYPE_INT_ARGB)
    val ig = img.createGraphics()
    ig.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
    val upper = title.toUpperCase
    val accent =
      if (upper.contains("OLD HOUSE")) new Color(223, 72, 82)
      else if (upper.contains("BABASAHEB")) new Color(242, 185, 71)
      else if (upper.contains("CRICKET")) new Color(56, 206, 137)
      else if (upper.contains("CHAT")) new Color(69, 209, 180)
      else if (upper.contains("ANIMATION")) new Color(176, 120, 255)
      else if (upper.contains("GAME")) new Color(255, 141, 75)
      else if (upper.contains("CODE")) new Color(79, 171, 255)
      else if (upper.contains("PAINT")) new Color(68, 197, 255)
      else if (upper.contains("CALCULATOR")) new Color(104, 218, 142)
      else if (upper.contains("WEB")) new Color(80, 170, 242)
      else if (upper.contains("SETTING")) new Color(145, 164, 195)
      else if (upper.contains("CLOCK")) new Color(248, 193, 88)
      else new Color(87, 190, 231)
    ig.setPaint(new GradientPaint(2f, 2f, accent.brighter(), 42f, 42f, accent.darker()))
    ig.fillRoundRect(2, 2, 40, 40, 13, 13)
    ig.setColor(new Color(255, 255, 255, 34)); ig.fillOval(5, 3, 33, 20)
    ig.setColor(new Color(6, 14, 32, 105)); ig.fillOval(9, 9, 27, 27)
    ig.setColor(new Color(255, 255, 255, 220)); ig.setStroke(new BasicStroke(1.7f))
    if (upper.contains("OLD HOUSE")) {
      val house = new Path2D.Double(); house.moveTo(10,22); house.lineTo(22,11); house.lineTo(34,22); house.lineTo(31,22); house.lineTo(31,34); house.lineTo(14,34); house.lineTo(14,22); house.closePath(); ig.draw(house)
      ig.drawRect(20,25,5,9)
    } else if (upper.contains("CRICKET")) {
      ig.drawLine(12, 30, 31, 12); ig.drawRoundRect(18, 12, 9, 17, 5, 5); ig.drawLine(20, 16, 24, 16); ig.drawLine(19, 21, 24, 21)
    } else if (upper.contains("CHAT")) {
      ig.drawRoundRect(9, 10, 26, 21, 7, 7); ig.drawLine(15, 31, 11, 36); ig.drawLine(11, 36, 22, 31)
    } else if (upper.contains("PAINT")) {
      ig.drawOval(10, 9, 25, 25); ig.setColor(accent.brighter()); ig.fillOval(15,14,5,5); ig.fillOval(25,15,5,5); ig.fillOval(20,25,5,5)
    } else if (upper.contains("ANIMATION")) {
      ig.drawOval(8,8,28,28); val play = new Path2D.Double(); play.moveTo(18,14); play.lineTo(29,22); play.lineTo(18,30); play.closePath(); ig.fill(play)
    } else if (upper.contains("CODE")) {
      ig.setFont(new Font("Consolas", Font.BOLD, 15)); ig.drawString("</>", 8, 27)
    } else if (upper.contains("CALCULATOR")) {
      ig.drawRoundRect(12,7,20,30,4,4); ig.drawRect(15,11,14,6); ig.drawLine(16,23,28,23); ig.drawLine(16,28,28,28); ig.drawLine(16,33,28,33)
    } else if (upper.contains("WEB")) {
      ig.drawOval(9,9,26,26); ig.drawOval(16,9,12,26); ig.drawLine(10,22,34,22); ig.drawArc(11,12,22,20,0,180)
    } else if (upper.contains("CLOCK")) {
      ig.drawOval(9,9,26,26); ig.drawLine(22,13,22,22); ig.drawLine(22,22,29,26)
    } else if (upper.contains("SETTING")) {
      ig.drawOval(10,10,24,24); ig.drawOval(17,17,10,10); for (a <- 0 until 8) { val ang=a*math.Pi/4; ig.drawLine((22+math.cos(ang)*13).toInt,(22+math.sin(ang)*13).toInt,(22+math.cos(ang)*16).toInt,(22+math.sin(ang)*16).toInt) }
    } else if (upper.contains("BABASAHEB")) {
      ig.drawOval(13,7,18,18); ig.drawRoundRect(10,25,24,12,5,5); ig.drawLine(22,27,22,34)
    } else if (upper.contains("GAME")) {
      ig.drawRoundRect(8,13,28,19,8,8); ig.drawLine(15,22,22,22); ig.drawLine(18,19,18,25); ig.fillOval(27,19,3,3); ig.fillOval(31,24,3,3)
    } else {
      ig.setFont(new Font("Arial", Font.BOLD, 21)); val glyph=upper.headOption.map(_.toString).getOrElse("A"); ig.drawString(glyph, 15, 29)
    }
    ig.dispose()
    new ImageIcon(img)
  }

  def iconButton(title: String, x: Int, y: Int): JButton = {
    val b = new JButton(title) {
      override protected def paintComponent(graphics: Graphics): Unit = {
        val gg = graphics.create().asInstanceOf[Graphics2D]
        gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
        val hot = getModel.isRollover || getModel.isPressed
        val top = if (hot) new Color(38, 70, 110, 240) else new Color(20, 34, 59, 224)
        val bottom = if (hot) new Color(22, 49, 88, 245) else new Color(10, 20, 41, 222)
        gg.setPaint(new GradientPaint(0f, 0f, top, 0f, getHeight, bottom))
        gg.fillRoundRect(0, 0, getWidth, getHeight, 15, 15)
        gg.setColor(new Color(140, 202, 255, if (hot) 155 else 70))
        gg.drawRoundRect(0, 0, getWidth - 1, getHeight - 1, 15, 15)
        gg.setColor(new Color(255,255,255,22))
        gg.drawLine(10, 3, getWidth - 10, 3)
        gg.dispose()
        super.paintComponent(graphics)
      }
    }
    b.setSize(158, 60)
    b.setLocation(x, y)
    b.setFont(new Font("Arial", Font.BOLD, 11))
    b.setFocusable(false)
    b.setForeground(Color.WHITE)
    b.setIcon(desktopTileIcon(title))
    b.setIconTextGap(8)
    b.setHorizontalAlignment(SwingConstants.LEFT)
    b.setHorizontalTextPosition(SwingConstants.RIGHT)
    b.setContentAreaFilled(false)
    b.setOpaque(false)
    b.setBorder(new javax.swing.border.EmptyBorder(4, 6, 4, 6))
    b.setToolTipText("Open " + title)
    b
  }

  val paintApp = iconButton("PAINT STUDIO", 90, 110)
  val animationApp = iconButton("ANIMATION", 260, 110)
  val gameApp = iconButton("GAME STUDIO", 430, 110)
  val codeApp = iconButton("CODE DESK", 600, 110)
  val notesApp = iconButton("NOTEPAD", 770, 110)

  val calcApp = iconButton("CALCULATOR", 90, 185)
  val browserApp = iconButton("WEB BROWSER", 260, 185)
  val settingsApp = iconButton("SETTINGS", 430, 185)
  val clockApp = iconButton("CLOCK", 600, 185)

  // Newly integrated apps and game. These buttons belong to the simulated desktop.
  val liveChatApp = iconButton("YADNESH LIVE CHAT", 90, 260)
  val oldHouseGameApp = iconButton("OLD HOUSE 3D", 260, 260)
  val babasahebStudioApp = iconButton("BABASAHEB 3D", 430, 260)
  val cricketLiveApp = iconButton("CRICKET LIVE", 600, 260)

  val desktopApps = Array(
    paintApp,
    animationApp,
    gameApp,
    codeApp,
    notesApp,
    calcApp,
    browserApp,
    settingsApp,
    clockApp,
    liveChatApp,
    oldHouseGameApp,
    babasahebStudioApp,
    cricketLiveApp
  )

  var i0 = 0
  while (i0 < desktopApps.length) {
    desktopApps(i0).setVisible(false)
    screen.add(desktopApps(i0))
    i0 += 1
  }

  // Start panel (overlay-like Swing panel on screen)
  val startPanel = new JPanel()
  startPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 8, 8))
  startPanel.setBounds(20, 245, 560, 275)
  startPanel.setBackground(new Color(16, 20, 31, 245))

  val startTitle = new JLabel("  ULTRA START MENU  ")
  startTitle.setForeground(Color.WHITE)
  startTitle.setFont(new Font("Arial", Font.BOLD, 16))
  startPanel.add(startTitle)

  val startCalc = button("Calculator")
  val startNotes = button("Notepad")
  val startSettings = button("Settings")
  val startPaint = button("Paint")
  val startGame = button("Game Studio")
  val startAppsLibrary = button("Apps Library")
  val startGamesLibrary = button("Games Library")

  startPanel.add(startCalc)
  startPanel.add(startNotes)
  startPanel.add(startSettings)
  startPanel.add(startPaint)
  startPanel.add(startGame)
  startPanel.add(startAppsLibrary)
  startPanel.add(startGamesLibrary)
  startPanel.setVisible(false)
  screen.add(startPanel)

  def showDesktopIcons(show: Boolean): Unit = {
    var ix = 0
    while (ix < desktopApps.length) {
      desktopApps(ix).setVisible(show)
      ix += 1
    }
  }

  val desktopAnimationTimer = new Timer(33, new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      if (powered) {
        wallpaperTime += 0.033
        if (booting) {
          bootProgress = math.min(1.0, bootProgress + 0.018)
          if (bootProgress >= 1.0) {
            booting = false
            desktopMode = "DESKTOP"
            showDesktopIcons(true)
            topStatus.setText("POWER ON | DESKTOP READY | " + laptopBrand)
          }
        }
        screen.repaint()
        if (base != null) {
          base.repaint()
        }
      }
    }
  })
  desktopAnimationTimer.setCoalesce(true)

  // ==========================================================
  // GENERIC WINDOW BUILDER
  // ==========================================================

  def appWindow(title: String, width: Int, height: Int): JFrame = {
    val win = new JFrame(laptopBrand + " - " + title)
    win.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
    win.setSize(width, height)
    win.setLocationRelativeTo(laptopFrame)
    win.setLayout(new BorderLayout())
    win
  }


  // ==========================================================
  // APP / GAME CATALOGS
  // ==========================================================

  def openAppLibrary(initialSection: String): Unit = {
    val win = appWindow(if (initialSection == "GAMES") "GAMES LIBRARY" else "APPS LIBRARY", 760, 610)
    val tabs = new JTabbedPane()
    val appsPanel = new JPanel(new java.awt.GridLayout(0, 2, 10, 10))
    val gamesPanel = new JPanel(new java.awt.GridLayout(0, 2, 10, 10))
    appsPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(12, 12, 12, 12))
    gamesPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(12, 12, 12, 12))

    def addEntry(panel: JPanel, title: String, detail: String, run: () => Unit): Unit = {
      val card = new JPanel(new BorderLayout(6, 6))
      card.setBorder(javax.swing.BorderFactory.createCompoundBorder(
        javax.swing.BorderFactory.createLineBorder(new Color(80, 102, 143), 1),
        javax.swing.BorderFactory.createEmptyBorder(9, 9, 9, 9)
      ))
      val heading = new JLabel(title)
      heading.setFont(new Font("Arial", Font.BOLD, 14))
      val description = new JLabel("<html><body style='width:240px'>" + detail + "</body></html>")
      description.setFont(new Font("Arial", Font.PLAIN, 11))
      val launch = button("OPEN")
      launch.addActionListener(new ActionListener {
        override def actionPerformed(e: ActionEvent): Unit = {
          win.dispose()
          try run() catch { case ex: Throwable => javax.swing.JOptionPane.showMessageDialog(laptopFrame, "Could not launch " + title + ": " + ex.getMessage, "Launch Error", javax.swing.JOptionPane.ERROR_MESSAGE) }
        }
      })
      card.add(heading, BorderLayout.NORTH)
      card.add(description, BorderLayout.CENTER)
      card.add(launch, BorderLayout.SOUTH)
      panel.add(card)
    }

    addEntry(appsPanel, "Paint Studio", "Draw, shape, import media and edit artwork.", () => { if (mainFrame != null) { mainFrame.setLocationRelativeTo(laptopFrame); mainFrame.setVisible(true); canvas.requestFocusInWindow() } })
    addEntry(appsPanel, "Animation Studio", "Create frame-based animations.", () => openFrameAnimationStudio())
    addEntry(appsPanel, "Game Studio", "Open the built-in game creator.", () => openGameStudio())
    addEntry(appsPanel, "Code Desk", "Open the coding workspace.", () => { if (mainFrame != null) { mainFrame.setLocationRelativeTo(laptopFrame); mainFrame.setVisible(true); if (codeArea != null) codeArea.requestFocusInWindow() } })
    addEntry(appsPanel, "Notepad", "Create, open and save text documents.", () => openNotepad())
    addEntry(appsPanel, "Calculator", "Basic expression calculator.", () => openCalculator())
    addEntry(appsPanel, "Web Browser", "Open a URL in the system browser.", () => openBrowser())
    addEntry(appsPanel, "Settings", "Change laptop theme and brand.", () => openSettings())
    addEntry(appsPanel, "Clock", "Live clock and date.", () => openClock())
    addEntry(appsPanel, "Yadnesh Live Chat", "Friends, requests, profiles and LAN chat.", () => openYadneshLiveChat())
    addEntry(appsPanel, "Babasaheb 3D Statue Studio", "Inspect original procedural 3D statue models.", () => openBabasahebStudio())
    addEntry(appsPanel, "Cricket Live Center", "Cricket live-score dashboard and source link.", () => openCricketLiveCenter())

    addEntry(gamesPanel, "Old House 3D", "First-person exploration, locked exit, key and escape objective.", () => openOldHouseGame())
    addEntry(gamesPanel, "Game Studio", "Create and preview your own games.", () => openGameStudio())

    tabs.addTab("Apps", new JScrollPane(appsPanel))
    tabs.addTab("Games", new JScrollPane(gamesPanel))
    tabs.setSelectedIndex(if (initialSection == "GAMES") 1 else 0)
    win.add(tabs, BorderLayout.CENTER)
    win.setVisible(true)
  }

  // ==========================================================
  // NOTEPAD - REAL EDITING + OPEN + SAVE + NEW
  // ==========================================================

  def openNotepad(): Unit = {
    val win = appWindow("NOTEPAD", 850, 600)

    val text = new JTextArea()
    text.setFont(new Font("Monospaced", Font.PLAIN, 16))
    text.setLineWrap(true)
    text.setWrapStyleWord(true)

    val top = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 6))
    val newB = button("NEW")
    val openB = button("OPEN")
    val saveB = button("SAVE AS")
    val info = new JLabel("Ready")

    top.add(newB)
    top.add(openB)
    top.add(saveB)
    top.add(info)

    newB.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        text.setText("")
        info.setText("New document")
      }
    })

    openB.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        val chooser = new JFileChooser()
        if (chooser.showOpenDialog(win) == JFileChooser.APPROVE_OPTION) {
          val source = scala.io.Source.fromFile(chooser.getSelectedFile, "UTF-8")
          try {
            text.setText(source.mkString)
          } finally {
            source.close()
          }
          info.setText("Opened: " + chooser.getSelectedFile.getName)
        }
      }
    })

    saveB.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        val chooser = new JFileChooser()
        if (chooser.showSaveDialog(win) == JFileChooser.APPROVE_OPTION) {
          val out = new java.io.PrintWriter(chooser.getSelectedFile, "UTF-8")
          try {
            out.print(text.getText)
          } finally {
            out.close()
          }
          info.setText("Saved: " + chooser.getSelectedFile.getName)
        }
      }
    })

    win.add(top, BorderLayout.NORTH)
    win.add(new JScrollPane(text), BorderLayout.CENTER)
    win.setVisible(true)
    text.requestFocusInWindow()
  }

  // ==========================================================
  // CALCULATOR - REAL BUTTON INPUT + SAFE BASIC EXPRESSION MATH
  // ==========================================================

  def openCalculator(): Unit = {
    val win = appWindow("CALCULATOR", 400, 530)

    val display = new JTextField("0")
    display.setHorizontalAlignment(SwingConstants.RIGHT)
    display.setFont(new Font("Arial", Font.BOLD, 28))
    display.setEditable(false)

    val grid = new JPanel(new java.awt.GridLayout(5, 4, 6, 6))
    val values = Array(
      "7","8","9","/",
      "4","5","6","*",
      "1","2","3","-",
      "0",".","=","+",
      "C","(",")","%"
    )

    var current = ""

    def setDisplay(s: String): Unit = {
      display.setText(if (s.isEmpty) "0" else s)
    }

    def calculateSimple(expr: String): Double = {
      val clean = expr.replace(" ", "")

      def numbersAndOps: Vector[String] = {
        val out = ArrayBuffer[String]()
        var token = ""
        var j = 0
        while (j < clean.length) {
          val c = clean.charAt(j)
          if ((c >= '0' && c <= '9') || c == '.') {
            token += c
          } else {
            if (token.nonEmpty) out += token
            token = ""
            out += c.toString
          }
          j += 1
        }
        if (token.nonEmpty) out += token
        out.toVector
      }

      val tokens = numbersAndOps
      val nums = ArrayBuffer[Double]()
      val ops = ArrayBuffer[String]()
      var k = 0
      while (k < tokens.length) {
        if (tokens(k) != "+" && tokens(k) != "-" && tokens(k) != "*" && tokens(k) != "/") {
          nums += tokens(k).toDouble
        } else {
          ops += tokens(k)
        }
        k += 1
      }

      if (nums.isEmpty) 0.0
      else {
        var result = nums(0)
        var n = 1
        var o = 0
        while (n < nums.length && o < ops.length) {
          val op = ops(o)
          if (op == "+") result += nums(n)
          else if (op == "-") result -= nums(n)
          else if (op == "*") result *= nums(n)
          else if (op == "/") {
            if (nums(n) == 0.0) throw new ArithmeticException("Cannot divide by zero")
            result /= nums(n)
          }
          n += 1
          o += 1
        }
        result
      }
    }

    var ci = 0
    while (ci < values.length) {
      val label = values(ci)
      val b = button(label)
      b.setFont(new Font("Arial", Font.BOLD, 17))
      b.addActionListener(new ActionListener {
        override def actionPerformed(e: ActionEvent): Unit = {
          if (label == "C") {
            current = ""
            setDisplay(current)
          } else if (label == "=") {
            try {
              val result = calculateSimple(current)
              current = if (result % 1.0 == 0.0) result.toLong.toString else result.toString
              setDisplay(current)
            } catch {
              case _: Throwable =>
                current = ""
                setDisplay("ERROR")
            }
          } else if (label == "%") {
            try {
              val result = calculateSimple(current) / 100.0
              current = result.toString
              setDisplay(current)
            } catch {
              case _: Throwable =>
                setDisplay("ERROR")
            }
          } else {
            if (display.getText == "ERROR") current = ""
            current += label
            setDisplay(current)
          }
        }
      })
      grid.add(b)
      ci += 1
    }

    win.add(display, BorderLayout.NORTH)
    win.add(grid, BorderLayout.CENTER)
    win.setVisible(true)
  }

  // ==========================================================
  // WEB BROWSER - REAL SYSTEM BROWSER OPEN + URL ENTRY
  // ==========================================================

  def openBrowser(): Unit = {
    val win = appWindow("WEB BROWSER", 850, 560)
    val address = new JTextField("https://www.google.com")
    val goB = button("GO")
    val backB = button("HOME")
    val systemB = button("OPEN IN SYSTEM BROWSER")
    val message = new JTextArea()
    message.setEditable(false)
    message.setFont(new Font("Arial", Font.PLAIN, 16))
    message.setLineWrap(true)
    message.setWrapStyleWord(true)
    message.setText(
      "ULTRA WEB BROWSER\n\n" +
      "Type a web address above. GO opens it in the computer's default browser.\n\n" +
      "Example: https://www.google.com\nExample: https://www.wikipedia.org"
    )

    def browse(): Unit = {
      try {
        var u = address.getText.trim
        if (!u.contains("://")) u = "https://" + u
        java.awt.Desktop.getDesktop.browse(new java.net.URI(u))
        message.setText("Opened in the system browser:\n\n" + u)
      } catch {
        case ex: Throwable => message.setText("Could not open URL:\n\n" + ex.getMessage)
      }
    }

    goB.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = browse()
    })

    systemB.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = browse()
    })

    backB.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        address.setText("https://www.google.com")
      }
    })

    val top = new JPanel(new BorderLayout(6, 6))
    val controls = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 4))
    controls.add(goB)
    controls.add(backB)
    controls.add(systemB)
    top.add(address, BorderLayout.CENTER)
    top.add(controls, BorderLayout.SOUTH)

    win.add(top, BorderLayout.NORTH)
    win.add(new JScrollPane(message), BorderLayout.CENTER)
    win.setVisible(true)
  }

  // ==========================================================
  // SETTINGS - LIVE THEME / BRAND SETTINGS
  // ==========================================================

  def openSettings(): Unit = {
    val win = appWindow("SETTINGS", 600, 430)
    val panel = new JPanel(new java.awt.GridLayout(0, 1, 8, 8))

    val brand = new JTextField(laptopBrand)
    val colorInfo = new JLabel(
      "Current RGB: " + laptopColor.getRed + ", " + laptopColor.getGreen + ", " + laptopColor.getBlue
    )
    val choose = button("CHANGE THEME COLOR")
    val applyBrand = button("APPLY BRAND")
    val resetTheme = button("RESET THEME")
    val keyboardInfo = new JLabel("Keyboard mirror: ON | Mouse cursor: ON")

    applyBrand.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        val t = brand.getText.trim
        laptopBrand = if (t.isEmpty) "ULTRA X9" else t
        laptopFrame.setTitle(laptopBrand + " - ULTRA 3D WINDOWS LAPTOP")
        topStatus.setText("BRAND UPDATED: " + laptopBrand)
        screen.repaint()
      }
    })

    choose.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        val c = JColorChooser.showDialog(win, "CHOOSE WINDOWS THEME COLOR", laptopColor)
        if (c != null) {
          laptopColor = c
          colorInfo.setText(
            "Current RGB: " + laptopColor.getRed + ", " + laptopColor.getGreen + ", " + laptopColor.getBlue
          )
          screen.repaint()
        }
      }
    })

    resetTheme.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        laptopColor = new Color(48, 52, 60)
        brand.setText("ULTRA X9")
        laptopBrand = "ULTRA X9"
        colorInfo.setText("Current RGB: 48, 52, 60")
        screen.repaint()
      }
    })

    panel.add(new JLabel("WINDOWS STYLE SETTINGS"))
    panel.add(new JLabel("Laptop Brand"))
    panel.add(brand)
    panel.add(applyBrand)
    panel.add(choose)
    panel.add(colorInfo)
    panel.add(resetTheme)
    panel.add(keyboardInfo)

    win.add(panel, BorderLayout.CENTER)
    win.setVisible(true)
  }

  // ==========================================================
  // CLOCK
  // ==========================================================

  def openClock(): Unit = {
    val win = appWindow("CLOCK", 420, 260)
    val label = new JLabel("00:00:00", SwingConstants.CENTER)
    label.setFont(new Font("Arial", Font.BOLD, 44))

    val dateLabel = new JLabel("", SwingConstants.CENTER)
    dateLabel.setFont(new Font("Arial", Font.PLAIN, 17))

    val timer = new Timer(500, new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        label.setText(new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date()))
        dateLabel.setText(new java.text.SimpleDateFormat("EEEE, dd MMMM yyyy").format(new java.util.Date()))
      }
    })
    timer.start()

    win.add(label, BorderLayout.CENTER)
    win.add(dateLabel, BorderLayout.SOUTH)
    win.addWindowListener(new java.awt.event.WindowAdapter {
      override def windowClosed(e: java.awt.event.WindowEvent): Unit = timer.stop()
    })
    win.setVisible(true)
  }

  // ==========================================================
  // INTEGRATED APP LAUNCHERS
  // ==========================================================

  def openYadneshLiveChat(): Unit = {
    desktopMode = "YADNESH LIVE CHAT"
    topStatus.setText("OPENING YADNESH LIVE CHAT")
    screen.repaint()
    LiveChatApp.startApp()
  }

  def openOldHouseGame(): Unit = {
    desktopMode = "OLD HOUSE 3D"
    topStatus.setText("OLD HOUSE 3D GAME OPEN")
    screen.repaint()
    OldHouse3DImproved.start()
  }

  def openBabasahebStudio(): Unit = {
    desktopMode = "BABASAHEB 3D STATUE STUDIO"
    topStatus.setText("BABASAHEB 3D STATUE STUDIO OPEN")
    screen.repaint()
    Babasaheb3DStatue.start()
  }

  def openCricketLiveCenter(): Unit = {
    desktopMode = "CRICKET LIVE CENTER"
    topStatus.setText("CRICKET LIVE CENTER OPEN")
    screen.repaint()
    YTRCricketLiveCenterApp.start()
  }

  // ==========================================================
  // APP ACTIONS
  // ==========================================================

  paintApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      if (mainFrame != null) {
        mainFrame.setLocationRelativeTo(laptopFrame)
        mainFrame.setVisible(true)
        canvas.requestFocusInWindow()
      }
      desktopMode = "PAINT"
      topStatus.setText("PAINT STUDIO OPEN")
      screen.repaint()
    }
  })

  animationApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      openFrameAnimationStudio()
      desktopMode = "ANIMATION"
      topStatus.setText("ANIMATION STUDIO OPEN")
      screen.repaint()
    }
  })

  gameApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      openGameStudio()
      desktopMode = "GAME STUDIO"
      topStatus.setText("GAME STUDIO OPEN")
      screen.repaint()
    }
  })

  codeApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      if (mainFrame != null) {
        mainFrame.setLocationRelativeTo(laptopFrame)
        mainFrame.setVisible(true)
        if (codeArea != null) codeArea.requestFocusInWindow()
      }
      desktopMode = "CODE DESK"
      topStatus.setText("CODE DESK OPEN")
      screen.repaint()
    }
  })

  notesApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = openNotepad()
  })

  calcApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = openCalculator()
  })

  browserApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = openBrowser()
  })

  settingsApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = openSettings()
  })

  clockApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = openClock()
  })

  liveChatApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = openYadneshLiveChat()
  })

  oldHouseGameApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = openOldHouseGame()
  })

  babasahebStudioApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = openBabasahebStudio()
  })

  cricketLiveApp.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = openCricketLiveCenter()
  })

  // Start menu shortcuts
  startCalc.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      startPanel.setVisible(false)
      openCalculator()
    }
  })

  startNotes.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      startPanel.setVisible(false)
      openNotepad()
    }
  })

  startSettings.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      startPanel.setVisible(false)
      openSettings()
    }
  })

  startPaint.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      startPanel.setVisible(false)
      if (mainFrame != null) mainFrame.setVisible(true)
    }
  })

  startGame.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      startPanel.setVisible(false)
      openGameStudio()
    }
  })

  startAppsLibrary.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      startPanel.setVisible(false)
      openAppLibrary("APPS")
    }
  })

  startGamesLibrary.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      startPanel.setVisible(false)
      openAppLibrary("GAMES")
    }
  })

  // Laptop chassis is constructed below; the reference is declared above for animation safety.

  // ==========================================================
  // COLOR BUTTON
  // ==========================================================

  colorButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      val selected = JColorChooser.showDialog(
        laptopFrame,
        "CHOOSE LAPTOP / WINDOWS COLOR",
        laptopColor
      )
      if (selected != null) {
        laptopColor = selected
        screen.repaint()
        topBar.setBackground(laptopColor.darker())
        base.setBackground(laptopColor.darker())
        topStatus.setText(
          "THEME: RGB " +
          laptopColor.getRed + "," +
          laptopColor.getGreen + "," +
          laptopColor.getBlue
        )
      }
    }
  })

  brandField.addKeyListener(new KeyAdapter {
    override def keyReleased(e: KeyEvent): Unit = {
      val t = brandField.getText.trim
      laptopBrand = if (t.isEmpty) "ULTRA X9" else t
      laptopFrame.setTitle(laptopBrand + " - ULTRA 3D WINDOWS LAPTOP")
      screen.repaint()
    }
  })

  // ==========================================================
  // POWER BUTTON
  // ==========================================================

  // Salted SHA-256 verifier for the simulated laptop login.
  // The password itself is not embedded in source code.
  val laptopLoginSalt = "YTR-LAPTOP-LOGIN-SALT-2026"
  val laptopLoginVerifier = "2f52b3afad1d7edfe3df7406a24c03d0855be3b0ba69e8108bb839cd03944940"

  def laptopSha256Hex(value: String): String = {
    val digest = java.security.MessageDigest.getInstance("SHA-256")
    digest.digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8))
      .map(b => "%02x".format(b & 0xff)).mkString
  }

  def requestLaptopPassword(): Boolean = {
    val field = new javax.swing.JPasswordField(18)
    val result = javax.swing.JOptionPane.showConfirmDialog(
      laptopFrame,
      Array[AnyRef](new JLabel("Enter the laptop password to unlock the desktop:"), field),
      "YTR Laptop Secure Login",
      javax.swing.JOptionPane.OK_CANCEL_OPTION,
      javax.swing.JOptionPane.PLAIN_MESSAGE
    )
    val entered = new String(field.getPassword)
    field.setText("")
    result == javax.swing.JOptionPane.OK_OPTION &&
      laptopSha256Hex(laptopLoginSalt + entered) == laptopLoginVerifier
  }

  powerButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      if (!powered && !requestLaptopPassword()) {
        topStatus.setText("LOGIN CANCELLED OR INCORRECT PASSWORD")
        screen.repaint()
        return
      }
      if (!powered) {
        powered = true
        booting = true
        bootProgress = 0.0
        desktopMode = "BOOTING"
        val b = brandField.getText.trim
        laptopBrand = if (b.isEmpty) "ULTRA X9" else b
        laptopFrame.setTitle(laptopBrand + " - ULTRA 3D WINDOWS LAPTOP")
        powerButton.setText("POWER OFF")
        brandField.setEnabled(false)
        resetButton.setEnabled(true)
        startPanel.setVisible(false)
        showDesktopIcons(false)
        topStatus.setText("BOOT SEQUENCE | INITIALIZING DESKTOP GRAPHICS")
      } else {
        powered = false
        booting = false
        bootProgress = 0.0
        desktopMode = "DESKTOP"
        powerButton.setText("POWER ON")
        brandField.setEnabled(true)
        showDesktopIcons(false)
        startPanel.setVisible(false)
        topStatus.setText("POWER OFF | Press POWER ON")
      }

      screen.repaint()
      screen.requestFocusInWindow()
    }
  })

  resetButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      powered = false
      booting = false
      bootProgress = 0.0
      wallpaperTime = 0.0
      laptopBrand = "ULTRA X9"
      laptopColor = new Color(48, 52, 60)
      brandField.setText("ULTRA X9")
      brandField.setEnabled(true)
      powerButton.setText("POWER ON")
      showDesktopIcons(false)
      startPanel.setVisible(false)
      topStatus.setText("RESET COMPLETE | POWER OFF")
      screen.repaint()
    }
  })

  // ==========================================================
  // START CLICK AREA + MOUSE CURSOR MIRROR
  // ==========================================================

  screen.addMouseListener(new MouseAdapter {
    override def mousePressed(e: MouseEvent): Unit = {
      cursorX = e.getX
      cursorY = e.getY

      if (powered) {
        val sy = screen.getHeight - 64
        if (e.getY >= sy) {
          if (e.getX >= 20 && e.getX <= 115) {
            startOpen = !startOpen
            startPanel.setVisible(startOpen)
          } else if (e.getX >= 115 && e.getX <= 205) {
            openCalculator()
          }
        }
      }
      screen.repaint()
    }
  })

  screen.addMouseMotionListener(new MouseMotionAdapter {
    override def mouseMoved(e: MouseEvent): Unit = {
      cursorX = e.getX
      cursorY = e.getY
      screen.repaint()
    }

    override def mouseDragged(e: MouseEvent): Unit = {
      cursorX = e.getX
      cursorY = e.getY
      screen.repaint()
    }
  })

  // ==========================================================
  // VIRTUAL KEYBOARD
  // ==========================================================

  def baseKeyColor(): Color = {
    new Color(
      math.min(255, laptopColor.getRed + 24),
      math.min(255, laptopColor.getGreen + 24),
      math.min(255, laptopColor.getBlue + 24)
    )
  }

  def addKey(panel: JPanel, label: String, code: Int, width: Int): JButton = {
    val b = new JButton(label) {
      override protected def paintComponent(graphics: Graphics): Unit = {
        val gg = graphics.create().asInstanceOf[Graphics2D]
        gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
        val current = getBackground
        val hot = getModel.isRollover || getModel.isPressed
        val top = if (hot) current.brighter().brighter() else current.brighter()
        val bottom = if (hot) current.darker() else current.darker().darker()
        gg.setPaint(new GradientPaint(0f, 0f, top, 0f, getHeight, bottom))
        gg.fillRoundRect(1, 1, getWidth - 2, getHeight - 2, 8, 8)
        gg.setColor(new Color(255, 255, 255, if (hot) 125 else 50))
        gg.drawRoundRect(1, 1, getWidth - 3, getHeight - 3, 8, 8)
        gg.setColor(new Color(0, 0, 0, 65))
        gg.drawLine(5, getHeight - 2, getWidth - 6, getHeight - 2)
        gg.dispose()
        super.paintComponent(graphics)
      }
    }
    b.setPreferredSize(new Dimension(width, 32))
    b.setBackground(baseKeyColor())
    b.setForeground(new Color(231, 241, 255))
    b.setFocusable(false)
    b.setContentAreaFilled(false)
    b.setOpaque(false)
    b.setBorderPainted(false)
    b.setFont(new Font("Arial", Font.BOLD, if (label.length > 5) 9 else 11))

    b.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        b.setBackground(new Color(255, 190, 55))
        topStatus.setText("KEYBOARD: " + label)
        val t = new Timer(120, new ActionListener {
          override def actionPerformed(e2: ActionEvent): Unit = b.setBackground(baseKeyColor())
        })
        t.setRepeats(false)
        t.start()
        screen.requestFocusInWindow()
      }
    })

    panel.add(b)
    keyMap(code) = b
    b
  }

  def row(labels: Array[String], codes: Array[Int]): JPanel = {
    val p = new JPanel(new FlowLayout(FlowLayout.CENTER, 3, 2))
    p.setOpaque(false)
    var ri = 0
    while (ri < labels.length) {
      val width =
        if (labels(ri) == "SPACE") 260
        else if (labels(ri).length >= 6) 78
        else 45
      addKey(p, labels(ri), codes(ri), width)
      ri += 1
    }
    p
  }

  val keyboardPanel = new JPanel(new java.awt.GridLayout(6, 1, 4, 3))
  keyboardPanel.setBackground(new Color(25, 28, 34))

  keyboardPanel.add(row(
    Array("ESC","F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12"),
    Array(KeyEvent.VK_ESCAPE,KeyEvent.VK_F1,KeyEvent.VK_F2,KeyEvent.VK_F3,KeyEvent.VK_F4,KeyEvent.VK_F5,KeyEvent.VK_F6,KeyEvent.VK_F7,KeyEvent.VK_F8,KeyEvent.VK_F9,KeyEvent.VK_F10,KeyEvent.VK_F11,KeyEvent.VK_F12)
  ))

  keyboardPanel.add(row(
    Array("1","2","3","4","5","6","7","8","9","0","-","=","BACKSPACE"),
    Array(KeyEvent.VK_1,KeyEvent.VK_2,KeyEvent.VK_3,KeyEvent.VK_4,KeyEvent.VK_5,KeyEvent.VK_6,KeyEvent.VK_7,KeyEvent.VK_8,KeyEvent.VK_9,KeyEvent.VK_0,KeyEvent.VK_MINUS,KeyEvent.VK_EQUALS,KeyEvent.VK_BACK_SPACE)
  ))

  keyboardPanel.add(row(
    Array("TAB","Q","W","E","R","T","Y","U","I","O","P","[","]","\\"),
    Array(KeyEvent.VK_TAB,KeyEvent.VK_Q,KeyEvent.VK_W,KeyEvent.VK_E,KeyEvent.VK_R,KeyEvent.VK_T,KeyEvent.VK_Y,KeyEvent.VK_U,KeyEvent.VK_I,KeyEvent.VK_O,KeyEvent.VK_P,KeyEvent.VK_OPEN_BRACKET,KeyEvent.VK_CLOSE_BRACKET,KeyEvent.VK_BACK_SLASH)
  ))

  keyboardPanel.add(row(
    Array("CAPS","A","S","D","F","G","H","J","K","L",";","'","ENTER"),
    Array(KeyEvent.VK_CAPS_LOCK,KeyEvent.VK_A,KeyEvent.VK_S,KeyEvent.VK_D,KeyEvent.VK_F,KeyEvent.VK_G,KeyEvent.VK_H,KeyEvent.VK_J,KeyEvent.VK_K,KeyEvent.VK_L,KeyEvent.VK_SEMICOLON,KeyEvent.VK_QUOTE,KeyEvent.VK_ENTER)
  ))

  keyboardPanel.add(row(
    Array("SHIFT","Z","X","C","V","B","N","M",",",".","/","SHIFT"),
    Array(KeyEvent.VK_SHIFT,KeyEvent.VK_Z,KeyEvent.VK_X,KeyEvent.VK_C,KeyEvent.VK_V,KeyEvent.VK_B,KeyEvent.VK_N,KeyEvent.VK_M,KeyEvent.VK_COMMA,KeyEvent.VK_PERIOD,KeyEvent.VK_SLASH,KeyEvent.VK_SHIFT)
  ))

  keyboardPanel.add(row(
    Array("CTRL","ALT","SPACE","ALT","CTRL","INSERT","DELETE","HOME","END","LEFT","UP","DOWN","RIGHT"),
    Array(KeyEvent.VK_CONTROL,KeyEvent.VK_ALT,KeyEvent.VK_SPACE,KeyEvent.VK_ALT,KeyEvent.VK_CONTROL,KeyEvent.VK_INSERT,KeyEvent.VK_DELETE,KeyEvent.VK_HOME,KeyEvent.VK_END,KeyEvent.VK_LEFT,KeyEvent.VK_UP,KeyEvent.VK_DOWN,KeyEvent.VK_RIGHT)
  ))

  val keyboardDispatcher = new java.awt.KeyEventDispatcher {
    override def dispatchKeyEvent(e: KeyEvent): Boolean = {
      val target = keyMap.get(e.getKeyCode)
      if (e.getID == KeyEvent.KEY_PRESSED) {
        target.foreach { b => b.setBackground(new Color(255, 190, 55)) }
        if (powered) {
          topStatus.setText("REAL KEYBOARD: " + KeyEvent.getKeyText(e.getKeyCode))
        }
      } else if (e.getID == KeyEvent.KEY_RELEASED) {
        target.foreach { b => b.setBackground(baseKeyColor()) }
      }
      false
    }
  }

  java.awt.KeyboardFocusManager
    .getCurrentKeyboardFocusManager
    .addKeyEventDispatcher(keyboardDispatcher)

  // ==========================================================
  // LAPTOP BASE
  // ==========================================================

  val keyboardLabel = new JLabel(
    "BACKLIT KEYBOARD  |  REAL KEY MIRROR ON  |  PRECISION TOUCHPAD",
    SwingConstants.CENTER
  )
  keyboardLabel.setFont(new Font("Arial", Font.BOLD, 11))
  keyboardLabel.setForeground(new Color(196, 216, 245))
  keyboardLabel.setOpaque(false)

  keyboardPanel.setOpaque(false)

  val trackpadPanel = new JPanel() {
    setOpaque(false)
    override protected def paintComponent(graphics: Graphics): Unit = {
      super.paintComponent(graphics)
      val gg = graphics.create().asInstanceOf[Graphics2D]
      gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
      val tw = math.min(360, getWidth - 60)
      val tx = (getWidth - tw) / 2
      val ty = 5
      gg.setColor(new Color(3, 8, 16, 125))
      gg.fillRoundRect(tx - 2, ty - 2, tw + 4, 34, 14, 14)
      gg.setPaint(new GradientPaint(tx.toFloat, ty.toFloat, new Color(59, 69, 83),
        tx.toFloat, (ty + 30).toFloat, new Color(20, 28, 41)))
      gg.fillRoundRect(tx, ty, tw, 30, 12, 12)
      val pulse = (0.5 + 0.5 * math.sin(wallpaperTime * 2.0))
      gg.setColor(new Color(72, 191, 255, (45 + pulse * 55).toInt))
      gg.setStroke(new BasicStroke(1.2f))
      gg.drawRoundRect(tx, ty, tw, 30, 12, 12)
      gg.setColor(new Color(180, 219, 255, 55))
      gg.drawLine(tx + 14, ty + 4, tx + tw - 14, ty + 4)
      gg.dispose()
    }
  }
  trackpadPanel.setPreferredSize(new Dimension(400, 41))

  base = new JPanel(new BorderLayout()) {
    override protected def paintComponent(graphics: Graphics): Unit = {
      super.paintComponent(graphics)
      val gg = graphics.create().asInstanceOf[Graphics2D]
      gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
      gg.setPaint(new GradientPaint(0f, 0f, laptopColor.brighter(), 0f, getHeight, laptopColor.darker().darker()))
      gg.fillRect(0, 0, getWidth, getHeight)
      // Brushed-metal streaks and two hinge blocks make the base read as hardware.
      gg.setColor(new Color(255, 255, 255, 13))
      var yy = 7
      while (yy < getHeight) { gg.drawLine(0, yy, getWidth, yy); yy += 7 }
      gg.setColor(new Color(3, 6, 12, 175))
      gg.fillRoundRect(26, 1, math.max(65, getWidth / 5), 9, 8, 8)
      gg.fillRoundRect(getWidth - 26 - math.max(65, getWidth / 5), 1, math.max(65, getWidth / 5), 9, 8, 8)
      val pulse = (0.5 + 0.5 * math.sin(wallpaperTime * 1.7))
      gg.setColor(new Color(65, 181, 255, (15 + pulse * 20).toInt))
      gg.fillOval(getWidth / 2 - 260, 0, 520, math.max(18, getHeight / 2))
      gg.setColor(new Color(93, 169, 230, 95))
      gg.drawLine(14, 1, getWidth - 14, 1)
      gg.dispose()
    }
  }
  base.setBackground(new Color(31, 34, 41))
  base.add(keyboardLabel, BorderLayout.NORTH)
  base.add(keyboardPanel, BorderLayout.CENTER)
  base.add(trackpadPanel, BorderLayout.SOUTH)

  laptopFrame.add(topBar, BorderLayout.NORTH)
  laptopFrame.add(screen, BorderLayout.CENTER)
  laptopFrame.add(base, BorderLayout.SOUTH)

  laptopFrame.addWindowListener(new java.awt.event.WindowAdapter {
    override def windowClosed(e: java.awt.event.WindowEvent): Unit = {
      desktopAnimationTimer.stop()
      java.awt.KeyboardFocusManager
        .getCurrentKeyboardFocusManager
        .removeKeyEventDispatcher(keyboardDispatcher)
    }
  })

  laptopFrame.setVisible(true)
  desktopAnimationTimer.start()
  screen.requestFocusInWindow()
}



// ============================================================
// INTEGRATED STANDALONE APP MODULES
// ============================================================

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.Toolkit
import java.awt.datatransfer.DataFlavor
import java.awt.image.BufferedImage
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent

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.InetSocketAddress
import java.net.NetworkInterface
import java.net.ServerSocket
import java.net.Socket
import java.net.DatagramPacket
import java.net.DatagramSocket

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.event.DocumentEvent
import javax.swing.event.DocumentListener
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"
        )

      md.digest(
        value.getBytes(
          StandardCharsets.UTF_8
        )
      ).map(
        "%02x".format(_)
      ).mkString
    }


    def randomHex(
        size: Int
    ): String = {

      val bytes =
        new Array[Byte](size)

      random.nextBytes(
        bytes
      )

      bytes.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,
        maximum: Int
    ): String = {

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

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


    def validUsername(
        name: String
    ): Boolean = {

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


    def now(): String = {

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


    // --------------------------------------------------------
    // IMAGE -> BASE64
    // --------------------------------------------------------

    def imageToBase64(
        original: Image
    ): String = {

      if (original == null) {
        return ""
      }

      try {

        val width =
          math.max(
            1,
            original.getWidth(null)
          )

        val height =
          math.max(
            1,
            original.getHeight(null)
          )

        val image =
          new BufferedImage(
            width,
            height,
            BufferedImage.TYPE_INT_ARGB
          )

        val graphics =
          image.createGraphics()

        try {

          graphics.drawImage(
            original,
            0,
            0,
            null
          )

        } finally {

          graphics.dispose()
        }

        val out =
          new ByteArrayOutputStream()

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

        out.close()

        val data =
          out.toByteArray

        if (
          data.length >
            350 * 1024
        ) {

          ""

        } else {

          encoder.encodeToString(
            data
          )
        }

      } catch {

        case _: Throwable =>
          ""
      }
    }


    // --------------------------------------------------------
    // FILE -> BASE64
    // --------------------------------------------------------

    def fileToBase64(
        path: String
    ): String = {

      try {

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

        imageToBase64(
          image
        )

      } catch {

        case _: Throwable =>
          ""
      }
    }


    // --------------------------------------------------------
    // BASE64 -> IMAGE
    // --------------------------------------------------------

    def base64ToImage(
        value: String
    ): BufferedImage = {

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

        null

      } else {

        try {

          val bytes =
            decoder.decode(
              value
            )

          ImageIO.read(
            new ByteArrayInputStream(
              bytes
            )
          )

        } catch {

          case _: Throwable =>
            null
        }
      }
    }


    // --------------------------------------------------------
    // PASTE IMAGE FROM CLIPBOARD
    // --------------------------------------------------------

    def clipboardImageBase64():
        String = {

      try {

        val clipboard =
          Toolkit
            .getDefaultToolkit
            .getSystemClipboard

        if (
          clipboard.isDataFlavorAvailable(
            DataFlavor.imageFlavor
          )
        ) {

          val image =
            clipboard.getData(
              DataFlavor.imageFlavor
            ).asInstanceOf[Image]

          imageToBase64(
            image
          )

        } else {

          ""
        }

      } catch {

        case _: Throwable =>
          ""
      }
    }


    // --------------------------------------------------------
    // LAN IPv4
    // --------------------------------------------------------

    def localIPs():
        Seq[String] = {

      val values =
        mutable.ListBuffer[String]()

      try {

        val interfaces =
          NetworkInterface.getNetworkInterfaces()

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

          val network =
            interfaces.nextElement()

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

            val addresses =
              network.getInetAddresses()

            while (
              addresses.hasMoreElements
            ) {

              val address =
                addresses.nextElement()

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

                val ip =
                  address.getHostAddress()

                if (
                  !values.contains(
                    ip
                  )
                ) {

                  values +=
                    ip
                }
              }
            }
          }
        }

      } catch {

        case _: Throwable =>
      }

      if (
        values.isEmpty
      ) {

        Seq(
          "127.0.0.1"
        )

      } else {

        values.toSeq
      }
    }


    def setupFont(): 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
      )
    }
  }


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

  case class UserRecord(
      username: String,
      passwordSalt: String,
      passwordVerifier: String,
      bio: String,
      avatar: String,
      friends: Set[String] = Set.empty[String],
      incomingRequests: Set[String] = Set.empty[String],
      blocked: Set[String] = Set.empty[String]
  )


  case class ChatMessage(
      sender: String,
      text: String,
      time: String
  )


  case class PersonItem(
      username: String,
      bio: String,
      online: Boolean,
      relation: String = "NONE"
  ) {

    override def toString: String = {

      val state =
        if (online) "? ONLINE" else "? OFFLINE"

      val rel =
        relation match {
          case "FRIEND"   => "   ? FRIEND"
          case "INCOMING" => "   ? REQUEST"
          case "OUTGOING" => "   ? REQUEST SENT"
          case "BLOCKED"  => "   ? BLOCKED"
          case _          => ""
        }

      username +
        "   " +
        state +
        rel
    }
  }


  case class ChatItem(
      id: String,
      title: String,
      group: Boolean,
      unread: Int = 0
  ) {

    override def toString: String = {
      if (unread > 0) {
        title + "   (" + unread + " new)"
      } else {
        title
      }
    }
  }



  // ==========================================================
  // AUTOMATIC LAN DISCOVERY
  // ==========================================================
  object LanDiscovery {

    private val DISCOVERY_PORT =
      5051

    private val MAGIC =
      "YADNESH_LIVE_CHAT_SERVER|"

    @volatile private var beaconRunning =
      false

    private var beaconThread:
        Thread = null

    def startBeacon(
        tcpPort: Int
    ): Unit = {

      if (beaconRunning) {
        return
      }

      beaconRunning = true

      beaconThread =
        new Thread(
          new Runnable {

            override def run(): Unit = {

              var socket:
                  DatagramSocket = null

              try {

                socket =
                  new DatagramSocket()

                socket.setBroadcast(
                  true
                )

                while (
                  beaconRunning
                ) {

                  val bytes =
                    (
                      MAGIC +
                        tcpPort
                    ).getBytes(
                      StandardCharsets.UTF_8
                    )

                  val packet =
                    new DatagramPacket(
                      bytes,
                      bytes.length,
                      java.net.InetAddress
                        .getByName(
                          "255.255.255.255"
                        ),
                      DISCOVERY_PORT
                    )

                  try {
                    socket.send(packet)
                  } catch {
                    case _: Throwable =>
                  }

                  try {
                    Thread.sleep(
                      1000L
                    )
                  } catch {
                    case _: InterruptedException =>
                      beaconRunning = false
                  }
                }

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

                if (
                  socket != null
                ) {
                  try {
                    socket.close()
                  } catch {
                    case _: Throwable =>
                  }
                }
              }
            }
          },
          "Yadnesh-Chat-Beacon"
        )

      beaconThread.setDaemon(
        true
      )

      beaconThread.start()
    }

    def stopBeacon(): Unit = {

      beaconRunning = false

      if (
        beaconThread != null
      ) {
        try {
          beaconThread.interrupt()
        } catch {
          case _: Throwable =>
        }
      }

      beaconThread = null
    }

    def findServer(
        timeoutMs: Int
    ): Option[(String, Int)] = {

      var socket:
          DatagramSocket = null

      try {

        socket =
          new DatagramSocket(
            DISCOVERY_PORT
          )

        socket.setSoTimeout(
          math.max(
            100,
            timeoutMs
          )
        )

        val buffer =
          new Array[Byte](
            512
          )

        val packet =
          new DatagramPacket(
            buffer,
            buffer.length
          )

        socket.receive(
          packet
        )

        val text =
          new String(
            packet.getData,
            packet.getOffset,
            packet.getLength,
            StandardCharsets.UTF_8
          ).trim

        if (
          text.startsWith(
            MAGIC
          )
        ) {

          val portText =
            text.substring(
              MAGIC.length
            )

          try {

            val port =
              portText.toInt

            if (
              port >= 1024 &&
              port <= 65535
            ) {
              Some(
                (
                  packet
                    .getAddress
                    .getHostAddress,
                  port
                )
              )
            } else {
              None
            }

          } catch {
            case _: Throwable =>
              None
          }

        } else {
          None
        }

      } catch {
        case _: java.net.SocketTimeoutException =>
          None

        case _: Throwable =>
          None

      } finally {

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


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

  object Server {

    private val lock =
      new Object


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


    private val sessions =
      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 chatHistory =
      mutable.Map[
        String,
        mutable.ListBuffer[
          ChatMessage
        ]
      ]()


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


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


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


    // --------------------------------------------------------
    // RECOVERY SECRET HASH
    // --------------------------------------------------------
    //
    // This is the SHA-256 verifier of the private recovery
    // secret. The secret itself is not shown in the UI/code.
    //
    // --------------------------------------------------------

    private val recoveryVerifier =
      "7f3f667469f4945d7b63b6831e26f1bd84bd02d57b455542b6144fe2b2a6a92d"


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


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


    private var serverSocket:
        ServerSocket = null


    private var running =
      false


    // ========================================================
    // 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(
              profilesFolder
            )

            loadProfiles()

            serverSocket =
              new ServerSocket()

            serverSocket.setReuseAddress(
              true
            )

            // Important:
            // listen on ALL network interfaces.
            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()

            LanDiscovery.startBeacon(
              port
            )

            true

          } catch {

            case _: Throwable =>

              running =
                false

              false
          }
        }
      }
    }


    // ========================================================
    // ACCEPT LOOP
    // ========================================================

    private def acceptLoop():
        Unit = {

      while (
        running
      ) {

        try {

          val sock =
            serverSocket.accept()

          sock.setKeepAlive(
            true
          )

          sock.setTcpNoDelay(
            true
          )


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


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


          val client =
            new ClientConnection(
              sock,
              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 = {

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


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

      val props =
        new Properties()


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


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


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


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


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

      props.setProperty(
        "friends",
        user.friends.toSeq.sorted.mkString(",")
      )

      props.setProperty(
        "incomingRequests",
        user.incomingRequests.toSeq.sorted.mkString(",")
      )

      props.setProperty(
        "blocked",
        user.blocked.toSeq.sorted.mkString(",")
      )


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


      try {

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

      } finally {

        out.close()
      }
    }


    // ========================================================
    // LOAD ALL ACCOUNTS
    // ========================================================

    private def loadProfiles():
        Unit = {

      users.clear()


      if (
        !Files.exists(
          profilesFolder
        )
      ) {

        return
      }


      val stream =
        Files.list(
          profilesFolder
        )


      try {

        val iterator =
          stream.iterator()


        while (
          iterator.hasNext
        ) {

          val path =
            iterator.next()


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

            try {

              val props =
                new Properties()


              val input =
                Files.newInputStream(
                  path
                )


              try {

                props.load(
                  input
                )

              } finally {

                input.close()
              }


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


              if (
                Util.validUsername(
                  username
                )
              ) {

                val friends =
                  props
                    .getProperty(
                      "friends",
                      ""
                    )
                    .split(",")
                    .map(_.trim)
                    .filter(_.nonEmpty)
                    .toSet

                val incomingRequests =
                  props
                    .getProperty(
                      "incomingRequests",
                      ""
                    )
                    .split(",")
                    .map(_.trim)
                    .filter(_.nonEmpty)
                    .toSet

                val blocked =
                  props
                    .getProperty(
                      "blocked",
                      ""
                    )
                    .split(",")
                    .map(_.trim)
                    .filter(_.nonEmpty)
                    .toSet

                val user =
                  UserRecord(

                    username,

                    props.getProperty(
                      "passwordSalt",
                      ""
                    ),

                    props.getProperty(
                      "passwordVerifier",
                      ""
                    ),

                    props.getProperty(
                      "bio",
                      ""
                    ),

                    props.getProperty(
                      "avatar",
                      ""
                    ),

                    friends,
                    incomingRequests,
                    blocked
                  )


                users.put(
                  username.toLowerCase,
                  user
                )
              }

            } catch {

              case _: Throwable =>
            }
          }
        }

      } finally {

        stream.close()
      }
    }


    // ========================================================
    // FRIENDSHIP HELPERS
    // ========================================================

    private def canonicalUser(
        username: String
    ): Option[String] = {

      users.get(
        username.toLowerCase
      ).map(_.username)
    }


    private def isFriend(
        a: String,
        b: String
    ): Boolean = {

      users
        .get(a.toLowerCase)
        .exists(_.friends.exists(_.equalsIgnoreCase(b)))
    }


    private def isBlocked(
        a: String,
        b: String
    ): Boolean = {

      users
        .get(a.toLowerCase)
        .exists(_.blocked.exists(_.equalsIgnoreCase(b)))
    }


    private def isMutuallyBlocked(
        a: String,
        b: String
    ): Boolean = {
      isBlocked(a, b) || isBlocked(b, a)
    }


    private def removeFriendshipBetween(
        a: String,
        b: String
    ): Unit = {
      users.get(a.toLowerCase).foreach { ua =>
        val updated = ua.copy(
          friends = ua.friends.filterNot(_.equalsIgnoreCase(b)),
          incomingRequests = ua.incomingRequests.filterNot(_.equalsIgnoreCase(b))
        )
        users.put(ua.username.toLowerCase, updated)
        saveProfile(updated)
      }
      users.get(b.toLowerCase).foreach { ub =>
        val updated = ub.copy(
          friends = ub.friends.filterNot(_.equalsIgnoreCase(a)),
          incomingRequests = ub.incomingRequests.filterNot(_.equalsIgnoreCase(a))
        )
        users.put(ub.username.toLowerCase, updated)
        saveProfile(updated)
      }
    }


    private def relationBetween(
        viewer: String,
        target: String
    ): String = {

      users.get(target.toLowerCase) match {

        case None =>
          "NONE"

        case Some(targetUser) =>

          if (
            isMutuallyBlocked(viewer, targetUser.username)
          ) {
            "BLOCKED"
          } else if (
            users
              .get(viewer.toLowerCase)
              .exists(_.friends.exists(_.equalsIgnoreCase(targetUser.username)))
          ) {
            "FRIEND"
          } else if (
            targetUser.incomingRequests.exists(_.equalsIgnoreCase(viewer))
          ) {
            "OUTGOING"
          } else if (
            users
              .get(viewer.toLowerCase)
              .exists(_.incomingRequests.exists(_.equalsIgnoreCase(targetUser.username)))
          ) {
            "INCOMING"
          } else {
            "NONE"
          }
      }
    }


    private def ensureDirectChat(
        a: String,
        b: String
    ): String = {

      val chatId =
        directChatId(
          a,
          b
        )

      addMember(
        chatId,
        a
      )

      addMember(
        chatId,
        b
      )

      chatGroups.put(
        chatId,
        false
      )

      chatNames.put(
        chatId,
        ""
      )

      chatHistory.getOrElseUpdate(
        chatId,
        mutable.ListBuffer[ChatMessage]()
      )

      chatId
    }


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

      users
        .get(username.toLowerCase)
        .foreach { user =>

          val friends =
            user.friends
              .flatMap(name => users.get(name.toLowerCase).map(_.username))
              .toSeq
              .sortBy(_.toLowerCase)

          val out =
            new StringBuilder(
              "FRIENDS_LIST|" + friends.length
            )

          friends.foreach { name =>
            val friendUser = users(name.toLowerCase)
            out.append("|").append(Util.encode(friendUser.username))
            out.append("|").append(
              if (sessions.contains(friendUser.username.toLowerCase)) "1" else "0"
            )
            out.append("|").append(Util.encode(friendUser.bio))
          }

          sendToUser(
            username,
            out.toString
          )
        }
    }


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

      users
        .get(username.toLowerCase)
        .foreach { user =>

          val requests =
            user.incomingRequests
              .flatMap(name => users.get(name.toLowerCase).map(_.username))
              .toSeq
              .sortBy(_.toLowerCase)

          val out =
            new StringBuilder(
              "FRIEND_REQUESTS|" + requests.length
            )

          requests.foreach { name =>
            val requester = users(name.toLowerCase)
            out.append("|").append(Util.encode(requester.username))
            out.append("|").append(
              if (sessions.contains(requester.username.toLowerCase)) "1" else "0"
            )
            out.append("|").append(Util.encode(requester.bio))
          }

          sendToUser(
            username,
            out.toString
          )
        }
    }


    // ========================================================
    // DIRECT CHAT ID
    // ========================================================

    private def directChatId(
        a: String,
        b: String
    ): String = {

      val pair =
        Seq(
          a.toLowerCase,
          b.toLowerCase
        ).sorted


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


    // ========================================================
    // ADD MEMBER
    // ========================================================

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

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

      set +=
        username
    }


    // ========================================================
    // SEND TO USER
    // ========================================================

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

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

            client.send(
              line
            )
        }
    }


    // ========================================================
    // BROADCAST CHAT
    // ========================================================

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

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

            members.foreach {
              username =>

                sendToUser(
                  username,
                  line
                )
            }
        }
    }


    // ========================================================
    // SEND CHAT TO USER
    // ========================================================

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

      val group =
        chatGroups.getOrElse(
          chatId,
          false
        )


      val title = {

        if (group) {

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

        } else {

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


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


          other match {

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

            case None =>
              "@Chat"
          }
        }
      }


      sendToUser(
        username,
        "CHAT|" +
          Util.encode(
            chatId
          ) +
          "|" +
          Util.encode(
            title
          ) +
          "|" +
          (
            if (group) "1"
            else "0"
          )
      )
    }


    // ========================================================
    // SEND ALL USER CHATS
    // ========================================================

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

      users
        .get(username.toLowerCase)
        .foreach { user =>

          user.friends.foreach { friendName =>
            users.get(friendName.toLowerCase).foreach { friend =>
              val chatId = ensureDirectChat(user.username, friend.username)
              addChatToUser(chatId, user.username)
            }
          }
        }

      chatMembers.keys.foreach {
        chatId =>

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

              if (
                members.contains(
                  username
                )
              ) {

                addChatToUser(
                  chatId,
                  username
                )
              }
            }
        }
    }


    // ========================================================
    // HISTORY
    // ========================================================

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

      val list =
        chatHistory.getOrElse(
          chatId,
          mutable.ListBuffer[
            ChatMessage
          ]()
        )


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


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


      val out =
        new StringBuilder()


      out.append(
        "HISTORY|"
      )


      out.append(
        Util.encode(
          chatId
        )
      )


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


      recent.foreach {
        message =>

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


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


          out.append(
            "|" +
              Util.encode(
                message.text
              )
          )
      }


      client.send(
        out.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 name =
          client.username


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

          sessions.get(
            name.toLowerCase
          ) match {

            case Some(current)
                if current eq client =>

              sessions.remove(
                name.toLowerCase
              )

              broadcastPresence(
                name
              )

            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 p =
        line.split(
          "\\|",
          -1
        )


      if (
        p.length == 0
      ) {

        return
      }


      val command =
        p(0)


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

      if (
        command ==
          "REGISTER_START"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


        val username =
          Util.decode(
            p(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(
                  "This account already exists. Login instead."
                )
            )

          } 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 (
          p.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(
                      "Account already exists."
                    )
                )

                registerChallenges.remove(
                  client.socket
                )

              } else {

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


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


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


                val validAvatar =
                  if (
                    avatar.length <=
                      480000
                  ) {
                    avatar
                  } else {
                    ""
                  }


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


                users.put(
                  username.toLowerCase,
                  user
                )


                saveProfile(
                  user
                )


                registerChallenges.remove(
                  client.socket
                )


                client.username =
                  username


                sessions.put(
                  username.toLowerCase,
                  client
                )


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


                sendAllUserChats(
                  username
                )

                sendFriendsList(
                  username
                )

                sendFriendRequestsList(
                  username
                )

                broadcastPresence(
                  username
                )
              }
          }
        }


        return
      }


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

      if (
        command ==
          "LOGIN_START"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


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


        lock.synchronized {

          users.get(
            username.toLowerCase
          ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Account not found. Create a new account."
                  )
              )


            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 (
          p.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(
                      p(2)
                    )


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


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

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


                    client.username =
                      username


                    sessions.put(
                      username.toLowerCase,
                      client
                    )


                    loginChallenges.remove(
                      client.socket
                    )


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


                    sendAllUserChats(
                      username
                    )

                    sendFriendsList(
                      username
                    )

                    sendFriendRequestsList(
                      username
                    )

                    broadcastPresence(
                      username
                    )

                  } else {

                    loginChallenges.remove(
                      client.socket
                    )


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


        return
      }


      // ======================================================
      // FORGOT PASSWORD START
      // ======================================================

      if (
        command ==
          "RECOVERY_START"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


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


        lock.synchronized {

          users.get(
            username.toLowerCase
          ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "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
      }


      // ======================================================
      // FORGOT PASSWORD FINISH
      // ======================================================

      if (
        command ==
          "RECOVERY_FINISH"
      ) {

        if (
          p.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(
                  p(2)
                )


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


              val newVerifier =
                Util.decode(
                  p(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
      }


      // ======================================================
      // REQUIRE LOGIN
      // ======================================================

      val currentUser =
        client.username


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

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

        return
      }


      // ======================================================
      // SEARCH PEOPLE
      // ======================================================

      if (
        command ==
          "SEARCH"
      ) {

        val query =
          if (
            p.length >= 2
          ) {

            Util.decode(
              p(1)
            ).trim.toLowerCase

          } else {

            ""
          }


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

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


        val result =
          new StringBuilder()


        result.append(
          "SEARCH_RESULT|" +
            found.length
        )


        found.foreach {
          user =>

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


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


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

            result.append(
              "|" +
                relationBetween(
                  currentUser,
                  user.username
                )
            )
        }


        client.send(
          result.toString
        )


        return
      }


      // ======================================================
      // GET PROFILE
      // ======================================================

      if (
        command ==
          "PROFILE"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


        val target =
          Util.decode(
            p(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 (
                    sessions.contains(
                      user.username.toLowerCase
                    )
                  ) {
                    "1"
                  } else {
                    "0"
                  }
                ) +
                "|" +
                relationBetween(
                  currentUser,
                  user.username
                )
            )
        }


        return
      }


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

      if (
        command ==
          "UPDATE_PROFILE"
      ) {

        if (
          p.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(
                    p(1)
                  ),
                  500
                )


              val avatar =
                Util.decode(
                  p(2)
                )


              val finalAvatar =
                if (
                  avatar.length <=
                    480000
                ) {
                  avatar
                } else {
                  oldUser.avatar
                }


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


              users.put(
                currentUser.toLowerCase,
                updated
              )


              saveProfile(
                updated
              )


              client.send(
                "PROFILE_SAVED"
              )


              onlineUsersNotify(
                currentUser
              )
          }
        }


        return
      }


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

      if (
        command ==
          "OPEN_DM"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


        val targetName =
          Util.decode(
            p(1)
          ).trim


        users.get(
          targetName.toLowerCase
        ) match {

          case None =>

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


          case Some(targetUser) =>

            if (
              targetUser.username.equalsIgnoreCase(
                currentUser
              )
            ) {

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

            } else if (
              isMutuallyBlocked(
                currentUser,
                targetUser.username
              )
            ) {

              client.send(
                "ERROR|" +
                  Util.encode(
                    "You cannot open this chat because one of the users is blocked."
                  )
              )

            } else if (
              !isFriend(
                currentUser,
                targetUser.username
              )
            ) {

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Become friends first: send and accept a friend request."
                  )
              )

            } else {

              lock.synchronized {

                val chatId =
                  ensureDirectChat(
                    currentUser,
                    targetUser.username
                  )

                addChatToUser(
                  chatId,
                  currentUser
                )

                addChatToUser(
                  chatId,
                  targetUser.username
                )

                sendHistory(
                  client,
                  chatId
                )
              }
            }
        }


        return
      }


      // ======================================================
      // TYPING INDICATOR
      // ======================================================

      if (
        command ==
          "TYPING"
      ) {

        if (p.length < 3) return

        val chatId = Util.decode(p(1))
        val state = if (p(2) == "1") "1" else "0"

        lock.synchronized {
          val allowed = chatMembers.get(chatId).exists(_.contains(currentUser))
          if (allowed) {
            broadcastChat(
              chatId,
              "TYPING|" +
                Util.encode(chatId) +
                "|" +
                Util.encode(currentUser) +
                "|" +
                state
            )
          }
        }
        return
      }


      // ======================================================
      // BLOCK / UNBLOCK
      // ======================================================

      if (command == "BLOCK_USER" || command == "UNBLOCK_USER") {

        if (p.length < 2) return

        val targetName = Util.decode(p(1)).trim

        lock.synchronized {
          users.get(targetName.toLowerCase) match {
            case None =>
              client.send("ERROR|" + Util.encode("User does not exist."))

            case Some(targetUser) =>
              if (targetUser.username.equalsIgnoreCase(currentUser)) {
                client.send("ERROR|" + Util.encode("You cannot block yourself."))
              } else {
                users.get(currentUser.toLowerCase).foreach { me =>
                  if (command == "BLOCK_USER") {
                    val updatedMe = me.copy(
                      blocked = me.blocked + targetUser.username,
                      friends = me.friends.filterNot(_.equalsIgnoreCase(targetUser.username)),
                      incomingRequests = me.incomingRequests.filterNot(_.equalsIgnoreCase(targetUser.username))
                    )
                    users.put(me.username.toLowerCase, updatedMe)
                    saveProfile(updatedMe)

                    val updatedTarget = targetUser.copy(
                      friends = targetUser.friends.filterNot(_.equalsIgnoreCase(me.username)),
                      incomingRequests = targetUser.incomingRequests.filterNot(_.equalsIgnoreCase(me.username))
                    )
                    users.put(targetUser.username.toLowerCase, updatedTarget)
                    saveProfile(updatedTarget)

                    client.send("BLOCK_STATE|" + Util.encode(targetUser.username) + "|1")
                    sendToUser(targetUser.username, "BLOCKED_BY|" + Util.encode(me.username))
                    sendFriendsList(me.username)
                    sendFriendsList(targetUser.username)
                    sendFriendRequestsList(me.username)
                    sendFriendRequestsList(targetUser.username)
                    searchNotifyAll()
                  } else {
                    val updatedMe = me.copy(
                      blocked = me.blocked.filterNot(_.equalsIgnoreCase(targetUser.username))
                    )
                    users.put(me.username.toLowerCase, updatedMe)
                    saveProfile(updatedMe)
                    client.send("BLOCK_STATE|" + Util.encode(targetUser.username) + "|0")
                    sendToUser(targetUser.username, "UNBLOCKED_BY|" + Util.encode(me.username))
                    searchNotifyAll()
                  }
                }
              }
          }
        }
        return
      }


      // ======================================================
      // FRIEND REQUEST
      // ======================================================

      if (
        command ==
          "FRIEND_REQUEST"
      ) {

        if (p.length < 2) return

        val targetName = Util.decode(p(1)).trim

        lock.synchronized {
          users.get(targetName.toLowerCase) match {
            case None =>
              client.send("ERROR|" + Util.encode("User does not exist."))

            case Some(targetUser) =>
              if (targetUser.username.equalsIgnoreCase(currentUser)) {
                client.send("ERROR|" + Util.encode("You cannot add yourself."))
              } else if (isMutuallyBlocked(currentUser, targetUser.username)) {
                client.send("ERROR|" + Util.encode("Friend request cannot be sent because one of the users is blocked."))
              } else if (isFriend(currentUser, targetUser.username)) {
                client.send("ERROR|" + Util.encode("You are already friends."))
              } else if (targetUser.incomingRequests.exists(_.equalsIgnoreCase(currentUser))) {
                client.send("ERROR|" + Util.encode("Friend request already sent."))
              } else if (users.get(currentUser.toLowerCase).exists(_.incomingRequests.exists(_.equalsIgnoreCase(targetUser.username)))) {
                client.send("ERROR|" + Util.encode("This person already sent you a request. Accept it instead."))
              } else {
                val updatedTarget = targetUser.copy(
                  incomingRequests = targetUser.incomingRequests + currentUser
                )
                users.put(targetUser.username.toLowerCase, updatedTarget)
                saveProfile(updatedTarget)

                client.send(
                  "FRIEND_REQUEST_SENT|" + Util.encode(targetUser.username)
                )

                sendToUser(
                  targetUser.username,
                  "FRIEND_REQUEST_RECEIVED|" + Util.encode(currentUser)
                )
              }
          }
        }

        return
      }


      // ======================================================
      // FRIEND ACCEPT
      // ======================================================

      if (
        command ==
          "FRIEND_ACCEPT"
      ) {

        if (p.length < 2) return

        val requesterName = Util.decode(p(1)).trim

        lock.synchronized {
          users.get(requesterName.toLowerCase) match {
            case None =>
              client.send("ERROR|" + Util.encode("User does not exist."))

            case Some(requester) =>
              users.get(currentUser.toLowerCase) match {
                case None =>
                  client.send("ERROR|" + Util.encode("Current account not found."))

                case Some(me) =>
                  val requestExists = me.incomingRequests.exists(_.equalsIgnoreCase(requester.username))
                  if (!requestExists) {
                    client.send("ERROR|" + Util.encode("No pending friend request from this person."))
                  } else {
                    val updatedMe = me.copy(
                      friends = me.friends + requester.username,
                      incomingRequests = me.incomingRequests.filterNot(_.equalsIgnoreCase(requester.username))
                    )
                    val updatedRequester = requester.copy(
                      friends = requester.friends + me.username
                    )

                    users.put(me.username.toLowerCase, updatedMe)
                    users.put(requester.username.toLowerCase, updatedRequester)
                    saveProfile(updatedMe)
                    saveProfile(updatedRequester)

                    val chatId = ensureDirectChat(me.username, requester.username)
                    addChatToUser(chatId, me.username)
                    addChatToUser(chatId, requester.username)

                    client.send(
                      "FRIEND_ACCEPTED|" + Util.encode(requester.username)
                    )
                    sendToUser(
                      requester.username,
                      "FRIEND_ACCEPTED|" + Util.encode(me.username)
                    )

                    sendFriendsList(me.username)
                    sendFriendRequestsList(me.username)
                    sendFriendsList(requester.username)
                    sendFriendRequestsList(requester.username)
                  }
              }
          }
        }

        return
      }


      // ======================================================
      // FRIEND REJECT
      // ======================================================

      if (
        command ==
          "FRIEND_REJECT"
      ) {

        if (p.length < 2) return

        val requesterName = Util.decode(p(1)).trim

        lock.synchronized {
          users.get(currentUser.toLowerCase) match {
            case Some(me) =>
              val filtered = me.incomingRequests.filterNot(_.equalsIgnoreCase(requesterName))
              val updated = me.copy(incomingRequests = filtered)
              users.put(me.username.toLowerCase, updated)
              saveProfile(updated)
              sendFriendRequestsList(me.username)
              sendToUser(
                requesterName,
                "FRIEND_REQUEST_REJECTED|" + Util.encode(me.username)
              )
            case None =>
          }
        }

        return
      }


      // ======================================================
      // REMOVE FRIEND
      // ======================================================

      if (
        command ==
          "FRIEND_REMOVE"
      ) {

        if (p.length < 2) return

        val targetName = Util.decode(p(1)).trim

        lock.synchronized {
          users.get(targetName.toLowerCase) match {
            case Some(targetUser) =>
              users.get(currentUser.toLowerCase) match {
                case Some(me) =>
                  val updatedMe = me.copy(
                    friends = me.friends.filterNot(_.equalsIgnoreCase(targetUser.username))
                  )
                  val updatedTarget = targetUser.copy(
                    friends = targetUser.friends.filterNot(_.equalsIgnoreCase(me.username))
                  )
                  users.put(me.username.toLowerCase, updatedMe)
                  users.put(targetUser.username.toLowerCase, updatedTarget)
                  saveProfile(updatedMe)
                  saveProfile(updatedTarget)
                  client.send("FRIEND_REMOVED|" + Util.encode(targetUser.username))
                  sendToUser(targetUser.username, "FRIEND_REMOVED|" + Util.encode(me.username))
                  sendFriendsList(me.username)
                  sendFriendsList(targetUser.username)
                case None =>
              }
            case None =>
              client.send("ERROR|" + Util.encode("User does not exist."))
          }
        }

        return
      }


      // ======================================================
      // FRIENDS LIST
      // ======================================================

      if (
        command ==
          "FRIENDS_LIST"
      ) {
        lock.synchronized {
          sendFriendsList(currentUser)
        }
        return
      }


      // ======================================================
      // FRIEND REQUESTS LIST
      // ======================================================

      if (
        command ==
          "FRIEND_REQUESTS"
      ) {
        lock.synchronized {
          sendFriendRequestsList(currentUser)
        }
        return
      }


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

      if (
        command ==
          "CREATE_GROUP"
      ) {

        if (
          p.length < 3
        ) {
          return
        }


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


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


        if (
          groupName.isEmpty ||
          count < 1 ||
          count > 20 ||
          p.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(
                p(
                  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(
                  "At least one other person is required."
                )
            )


          } else {

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


            chatMembers.put(
              chatId,
              members
            )


            chatGroups.put(
              chatId,
              true
            )


            chatNames.put(
              chatId,
              groupName
            )


            chatHistory.put(
              chatId,
              mutable.ListBuffer[
                ChatMessage
              ]()
            )


            members.foreach {
              member =>

                addChatToUser(
                  chatId,
                  member
                )
            }


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


        return
      }


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

      if (
        command ==
          "OPEN_CHAT"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


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


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


        if (
          allowed
        ) {

          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 (
          p.length < 3
        ) {
          return
        }


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


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


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

        val directBlocked =
          if (chatId.startsWith("DM:")) {
            chatMembers.get(chatId).exists { members =>
              members.exists { member =>
                !member.equalsIgnoreCase(currentUser) &&
                isMutuallyBlocked(currentUser, member)
              }
            }
          } else {
            false
          }


        if (
          allowed &&
          !directBlocked &&
          message.nonEmpty
        ) {

          val item =
            ChatMessage(
              currentUser,
              message,
              Util.now()
            )


          val list =
            chatHistory.getOrElseUpdate(
              chatId,
              mutable.ListBuffer[
                ChatMessage
              ]()
            )


          list +=
            item


          while (
            list.length > 300
          ) {

            list.remove(0)
          }


          broadcastChat(
            chatId,
            "MESSAGE|" +
              Util.encode(
                chatId
              ) +
              "|" +
              Util.encode(
                item.sender
              ) +
              "|" +
              Util.encode(
                item.time
              ) +
              "|" +
              Util.encode(
                item.text
              )
          )
        }


        return
      }


      // ======================================================
      // LOGOUT
      // ======================================================

      if (command == "LOGOUT") {
        lock.synchronized {
          val name = client.username
          if (name != null && name.nonEmpty) {
            sessions.get(name.toLowerCase) match {
              case Some(current) if current eq client =>
                sessions.remove(name.toLowerCase)
                broadcastPresence(name)
              case _ =>
            }
          }
        }
        client.send("LOGOUT_OK")
        client.close()
        return
      }


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

      if (
        command ==
          "PING"
      ) {

        client.send(
          "PONG"
        )

        return
      }
    }


    private def searchNotifyAll(): Unit = {
      sessions.values.foreach { c =>
        if (c.username != null && c.username.nonEmpty) {
          c.send("PROFILE_CHANGED|" + Util.encode(c.username))
        }
      }
    }


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

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

      val state =
        if (
          sessions.contains(
            username.toLowerCase
          )
        ) {
          "1"
        } else {
          "0"
        }


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


      sessions.values.foreach {
        client =>

          client.send(
            line
          )

          if (client.username != null && client.username.nonEmpty) {
            sendFriendsList(client.username)
          }
      }
    }


    // ========================================================
    // PROFILE NOTIFICATION
    // ========================================================

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

      sessions.values.foreach {
        client =>

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


  // ==========================================================
  // CLIENT
  // ==========================================================

  class ChatClient {

    var socket:
        Socket = null

    var reader:
        BufferedReader = null

    var writer:
        PrintWriter = null

    var connected =
      false

    @volatile private var heartbeatRunning =
      false

    @volatile private var intentionalClose =
      false


    var username =
      ""

    var password =
      ""


    var registerUsername =
      ""

    var registerPassword =
      ""

    var registerBio =
      ""

    var registerAvatar =
      ""


    var recoveryUsername =
      ""

    var recoverySecret =
      ""

    var recoveryNewPassword =
      ""


    var ownBio =
      ""

    var ownAvatar =
      ""


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


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

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

      intentionalClose = false

      if (
        connected
      ) {
        return true
      }


      try {

        socket =
          new Socket()


        socket.setKeepAlive(
          true
        )


        socket.setTcpNoDelay(
          true
        )


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


        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()

        startHeartbeat()


        true

      } catch {

        case _: Throwable =>

          connected =
            false

          false
      }
    }


    private def startHeartbeat(): Unit = {
      if (heartbeatRunning) return
      heartbeatRunning = true
      val t = new Thread(
        new Runnable {
          override def run(): Unit = {
            while (heartbeatRunning) {
              try {
                Thread.sleep(15000L)
              } catch {
                case _: Throwable =>
              }
              if (connected) {
                send("PING")
              }
            }
          }
        },
        "Yadnesh-Heartbeat"
      )
      t.setDaemon(true)
      t.start()
    }


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

    def send(
        line: String
    ): Unit = {

      if (
        connected &&
        writer != null
      ) {

        try {

          writer.println(
            line
          )

          writer.flush()

        } catch {

          case _: Throwable =>
        }
      }
    }


    // ========================================================
    // CLOSE
    // ========================================================

    def close(): Unit = {

      intentionalClose = true

      heartbeatRunning =
        false

      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(
              "Disconnected"
            )
          }


          val message =
            line


          SwingUtilities.invokeLater(
            new Runnable {

              override def run(): Unit = {

                processEvent(
                  message
                )
              }
            }
          )
        }

      } catch {

        case _: Throwable =>

          connected =
            false

          heartbeatRunning =
            false

          if (!intentionalClose) {
            try {
              SwingUtilities.invokeLater(
                new Runnable {
                  override def run(): Unit = {
                    callback("CLIENT_DISCONNECTED|" + Util.encode("Server connection lost."))
                  }
                }
              )
            } catch {
              case _: Throwable =>
            }
          }
      }
    }


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

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

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


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


      p(0) match {

        // ----------------------------------------------------
        // LOGIN CHALLENGE
        // ----------------------------------------------------

        case "LOGIN_CHALLENGE" =>

          if (
            p.length >= 3
          ) {

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


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


            val verifier =
              Util.sha256(
                salt +
                  password
              )


            val proof =
              Util.sha256(
                verifier +
                  nonce
              )


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


        // ----------------------------------------------------
        // REGISTER CHALLENGE
        // ----------------------------------------------------

        case "REGISTER_CHALLENGE" =>

          if (
            p.length >= 2
          ) {

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


            val verifier =
              Util.sha256(
                salt +
                  registerPassword
              )


            // Empty field kept for protocol compatibility.
            send(
              "REGISTER_FINISH|" +
                Util.encode(
                  registerUsername
                ) +
                "|" +
                Util.encode(
                  verifier
                ) +
                "|" +
                Util.encode(
                  registerBio
                ) +
                "|" +
                Util.encode(
                  registerAvatar
                ) +
                "|" +
                Util.encode(
                  "END"
                )
            )
          }


        // ----------------------------------------------------
        // RECOVERY CHALLENGE
        // ----------------------------------------------------

        case "RECOVERY_CHALLENGE" =>

          if (
            p.length >= 2
          ) {

            val nonce =
              Util.decode(
                p(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 (
            p.length >= 2
          ) {

            username =
              Util.decode(
                p(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 "FRIENDS_LIST" =>
          callback(line)


        case "FRIEND_REQUESTS" =>
          callback(line)


        case "FRIEND_REQUEST_SENT" =>
          callback(line)


        case "FRIEND_REQUEST_RECEIVED" =>
          callback(line)


        case "FRIEND_ACCEPTED" =>
          callback(line)


        case "FRIEND_REQUEST_REJECTED" =>
          callback(line)


        case "FRIEND_REMOVED" =>
          callback(line)


        case "TYPING" =>
          callback(line)


        case "BLOCK_STATE" =>
          callback(line)


        case "BLOCKED_BY" =>
          callback(line)


        case "UNBLOCKED_BY" =>
          callback(line)


        case "LOGOUT_OK" =>
          callback(line)


        case "CLIENT_DISCONNECTED" =>
          callback(line)


        case "PONG" =>
          ()


        case "GROUP_CREATED" =>
          callback(line)


        case "ERROR" =>

          val text =
            if (
              p.length >= 2
            ) {

              Util.decode(
                p(1)
              )

            } else {

              "Unknown error."
            }


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


        case _ =>
      }
    }
  }


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

  class LoginFrame {

    private val frame =
      new JFrame(
        APP_NAME
      )

    private val client =
      new ChatClient

    private val usernameField =
      new JTextField()

    private val passwordField =
      new JPasswordField()

    private val networkStatus =
      new JLabel(
        "NETWORK: STARTING...",
        SwingConstants.CENTER
      )

    @volatile private var networkReady =
      false

    private var discoveredHost =
      "127.0.0.1"

    private var discoveredPort =
      DEFAULT_PORT


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

    def show(): Unit = {

      Util.setupFont()

      frame.setTitle(
        APP_NAME +
          " - Login"
      )

      frame.setDefaultCloseOperation(
        WindowConstants.DISPOSE_ON_CLOSE
      )

      frame.setSize(
        760,
        560
      )

      frame.setLocationRelativeTo(
        null
      )

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

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


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

      val title =
        new JLabel(
          "<html>" +
            "<center>" +
            "<font size='6'><b>YADNESH LIVE CHAT</b></font><br>" +
            "<font size='3'>Automatic Wi-Fi PC-to-PC Live Chat</font>" +
            "</center>" +
            "</html>",
          SwingConstants.CENTER
        )

      root.add(
        title,
        BorderLayout.NORTH
      )


      // ------------------------------------------------------
      // LOGIN FORM
      // ------------------------------------------------------

      val form =
        new JPanel(
          new GridLayout(
            2,
            2,
            10,
            10
          )
        )

      form.setBorder(
        BorderFactory.createEmptyBorder(
          30,
          30,
          20,
          30
        )
      )

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

      form.add(
        usernameField
      )

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

      form.add(
        passwordField
      )

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

      center.add(
        form,
        BorderLayout.CENTER
      )

      networkStatus.setBorder(
        BorderFactory.createEmptyBorder(
          8,
          8,
          8,
          8
        )
      )

      center.add(
        networkStatus,
        BorderLayout.SOUTH
      )

      root.add(
        center,
        BorderLayout.CENTER
      )


      // ------------------------------------------------------
      // BUTTONS
      // ------------------------------------------------------

      val loginButton =
        new JButton(
          "LOGIN"
        )

      val registerButton =
        new JButton(
          "CREATE NEW ACCOUNT"
        )

      val forgotButton =
        new JButton(
          "FORGOT PASSWORD"
        )

      val buttons =
        new JPanel(
          new GridLayout(
            1,
            3,
            8,
            8
          )
        )

      buttons.add(
        loginButton
      )

      buttons.add(
        registerButton
      )

      buttons.add(
        forgotButton
      )

      root.add(
        buttons,
        BorderLayout.SOUTH
      )


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

      loginButton.addActionListener(
        new ActionListener {

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

            doLogin()
          }
        }
      )


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

      registerButton.addActionListener(
        new ActionListener {

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

            doRegister()
          }
        }
      )


      // ======================================================
      // FORGOT PASSWORD
      // ======================================================

      forgotButton.addActionListener(
        new ActionListener {

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

            forgotPassword()
          }
        }
      )


      frame.setContentPane(
        root
      )

      frame.setVisible(
        true
      )

      startAutomaticNetwork()
    }


    // ========================================================
    // AUTOMATIC NETWORK
    // ========================================================

    private def startAutomaticNetwork(): Unit = {

      networkStatus.setText(
        "NETWORK: SEARCHING FOR HOST PC..."
      )

      val thread =
        new Thread(
          new Runnable {

            override def run(): Unit = {

              var found:
                  Option[(String, Int)] =
                None

              val deadline =
                System.currentTimeMillis() +
                  2500L

              while (
                found.isEmpty &&
                System.currentTimeMillis() <
                  deadline
              ) {

                found =
                  LanDiscovery.findServer(
                    700
                  )
              }

              found match {

                case Some(
                    (host, port)
                    ) =>

                  discoveredHost =
                    host

                  discoveredPort =
                    port

                  val ok =
                    client.connect(
                      host,
                      port
                    )

                  networkReady =
                    ok

                  SwingUtilities.invokeLater(
                    new Runnable {

                      override def run(): Unit = {

                        if (ok) {

                          networkStatus.setText(
                            "NETWORK: CONNECTED AUTOMATICALLY ? " +
                              host +
                              ":" +
                              port
                          )

                        } else {

                          networkStatus.setText(
                            "NETWORK: HOST FOUND BUT CONNECTION FAILED"
                          )
                        }
                      }
                    }
                  )

                case None =>

                  // No host was found, so this PC becomes
                  // the first host automatically.
                  val started =
                    Server.start(
                      DEFAULT_PORT
                    )

                  if (
                    started
                  ) {

                    discoveredHost =
                      "127.0.0.1"

                    discoveredPort =
                      DEFAULT_PORT

                    val ok =
                      client.connect(
                        "127.0.0.1",
                        DEFAULT_PORT
                      )

                    networkReady =
                      ok

                    SwingUtilities.invokeLater(
                      new Runnable {

                        override def run(): Unit = {

                          if (ok) {

                            networkStatus.setText(
                              "NETWORK: THIS PC IS HOST ? " +
                                Util.localIPs().mkString(
                                  ", "
                                ) +
                                ":" +
                                DEFAULT_PORT
                            )

                          } else {

                            networkStatus.setText(
                              "NETWORK: HOST STARTED BUT LOCAL CONNECTION FAILED"
                            )
                          }
                        }
                      }
                    )

                  } else {

                    // A server may have appeared just after the
                    // discovery timeout. Try discovery once more.
                    val retry =
                      LanDiscovery.findServer(
                        1500
                      )

                    retry match {

                      case Some(
                          (host, port)
                          ) =>

                        discoveredHost =
                          host

                        discoveredPort =
                          port

                        val ok =
                          client.connect(
                            host,
                            port
                          )

                        networkReady =
                          ok

                        SwingUtilities.invokeLater(
                          new Runnable {

                            override def run(): Unit = {

                              networkStatus.setText(
                                if (ok) {
                                  "NETWORK: CONNECTED ? " +
                                    host +
                                    ":" +
                                    port
                                } else {
                                  "NETWORK: CONNECTION FAILED"
                                }
                              )
                            }
                          }
                        )

                      case None =>

                        networkReady =
                          false

                        SwingUtilities.invokeLater(
                          new Runnable {

                            override def run(): Unit = {

                              networkStatus.setText(
                                "NETWORK: NOT AVAILABLE ? START HOST ON ONE PC"
                              )
                            }
                          }
                        )
                    }
                  }
              }
            }
          },
          "Yadnesh-Auto-Network"
        )

      thread.setDaemon(
        true
      )

      thread.start()
    }


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

    private def connect(): Boolean = {

      if (
        client.connected
      ) {
        return true
      }

      if (
        !networkReady
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Network is still starting. Please wait a moment.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return false
      }

      val ok =
        client.connect(
          discoveredHost,
          discoveredPort
        )

      if (
        !ok
      ) {

        networkReady =
          false

        networkStatus.setText(
          "NETWORK: DISCONNECTED"
        )
      }

      ok
    }


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

    private def doLogin(): Unit = {

      val name =
        usernameField
          .getText
          .trim

      val pass =
        new String(
          passwordField
            .getPassword
        )

      if (
        !Util.validUsername(
          name
        )
      ) {

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

        return
      }

      if (
        pass.isEmpty
      ) {

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

        return
      }

      if (
        connect()
      ) {

        client.username =
          name

        client.password =
          pass

        client.callback =
          loginEvent

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


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

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

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

        openMain()
      }
    }


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

    private def doRegister(): Unit = {

      val dialog =
        new JDialog(
          frame,
          "CREATE NEW ACCOUNT",
          true
        )

      dialog.setSize(
        680,
        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 choose =
        new JButton(
          "CHOOSE IMAGE"
        )

      val paste =
        new JButton(
          "PASTE IMAGE"
        )

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

      var avatar =
        ""


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

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

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

      form.add(
        username
      )

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

      form.add(
        password
      )

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

      form.add(
        confirm
      )

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

      form.add(
        bio
      )

      form.add(
        new JLabel(
          "Profile Image:"
        )
      )

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

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

      imageButtons.add(
        choose
      )

      imageButtons.add(
        paste
      )

      imagePanel.add(
        imageButtons,
        BorderLayout.WEST
      )

      imagePanel.add(
        imageLabel,
        BorderLayout.CENTER
      )

      form.add(
        imagePanel
      )

      val create =
        new JButton(
          "CREATE ACCOUNT"
        )

      form.add(
        new JLabel(
          ""
        )
      )

      form.add(
        create
      )


      choose.addActionListener(
        new ActionListener {

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

            val chooser =
              new JFileChooser()

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

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

              if (
                encoded.nonEmpty
              ) {

                avatar =
                  encoded

                imageLabel.setText(
                  "Image selected"
                )

              } else {

                JOptionPane.showMessageDialog(
                  dialog,
                  "Image invalid or larger than about 350 KB.",
                  APP_NAME,
                  JOptionPane.WARNING_MESSAGE
                )
              }
            }
          }
        }
      )


      paste.addActionListener(
        new ActionListener {

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

            val encoded =
              Util.clipboardImageBase64()

            if (
              encoded.nonEmpty
            ) {

              avatar =
                encoded

              imageLabel.setText(
                "Pasted image saved"
              )

            } else {

              JOptionPane.showMessageDialog(
                dialog,
                "Clipboard does not contain an image.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )
            }
          }
        }
      )


      create.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 needs 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 (
              !connect()
            ) {
              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(
        form
      )

      dialog.setVisible(
        true
      )
    }


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

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

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

        openMain()
      }
    }


    // ========================================================
    // FORGOT PASSWORD
    // ========================================================

    private def forgotPassword(): Unit = {

      val dialog =
        new JDialog(
          frame,
          "FORGOT PASSWORD",
          true
        )

      dialog.setSize(
        570,
        420
      )

      dialog.setLocationRelativeTo(
        frame
      )

      val username =
        new JTextField()

      val recovery =
        new JPasswordField()

      val newPassword =
        new JPasswordField()

      val confirm =
        new JPasswordField()

      val reset =
        new JButton(
          "RESET PASSWORD"
        )

      val note =
        new JLabel(
          "<html>" +
            "Enter your Username / ID, private recovery secret,<br>" +
            "and choose a new password." +
            "</html>"
        )

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

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

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

      form.add(
        note
      )

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

      form.add(
        username
      )

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

      form.add(
        recovery
      )

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

      form.add(
        newPassword
      )

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

      form.add(
        confirm
      )

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

      panel.add(
        form,
        BorderLayout.CENTER
      )

      panel.add(
        reset,
        BorderLayout.SOUTH
      )

      reset.addActionListener(
        new ActionListener {

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

            val name =
              username
                .getText
                .trim

            val secret =
              new String(
                recovery
                  .getPassword
              )

            val newPass =
              new String(
                newPassword
                  .getPassword
              )

            val confirmPass =
              new String(
                confirm
                  .getPassword
              )

            if (
              !Util.validUsername(
                name
              )
            ) {

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

              return
            }

            if (
              secret.isEmpty
            ) {

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

              return
            }

            if (
              newPass.length < 4
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "New password needs 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 (
              !connect()
            ) {
              return
            }

            client.recoveryUsername =
              name

            client.recoverySecret =
              secret

            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.",
          APP_NAME,
          JOptionPane.INFORMATION_MESSAGE
        )

        client.callback =
          loginEvent
      }
    }


    // ========================================================
    // 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[
        PersonItem
      ]()


    private val friendsModel =
      new DefaultListModel[
        PersonItem
      ]()


    private val requestModel =
      new DefaultListModel[
        PersonItem
      ]()


    private val chatModel =
      new DefaultListModel[
        ChatItem
      ]()


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


    private val friendsList =
      new JList[
        PersonItem
      ](
        friendsModel
      )


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


    private val searchField =
      new JTextField()


    private val chatArea =
      new JTextArea()


    private val messageField =
      new JTextField()


    private val sendButton =
      new JButton(
        "SEND"
      )

    private val exportButton =
      new JButton(
        "EXPORT CHAT"
      )

    private val muteButton =
      new JButton(
        "MUTE CHAT"
      )

    private val logoutButton =
      new JButton(
        "LOGOUT"
      )

    private val blockButton =
      new JButton(
        "BLOCK USER"
      )


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


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


    private val profileStatus =
      new JLabel(
        ""
      )

    private val typingLabel =
      new JLabel(
        ""
      )

    private val onlineSummary =
      new JLabel(
        "  LIVE"
      )


    private val profileBio =
      new JTextArea()


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


    private val openChatButton =
      new JButton(
        "OPEN FRIEND CHAT"
      )


    private val friendButton =
      new JButton(
        "SEND FRIEND REQUEST"
      )


    private val acceptButton =
      new JButton(
        "ACCEPT REQUEST"
      )


    private val rejectButton =
      new JButton(
        "REJECT REQUEST"
      )


    private val removeFriendButton =
      new JButton(
        "REMOVE FRIEND"
      )


    private val friendsButton =
      new JButton(
        "REFRESH FRIENDS"
      )


    private val requestsButton =
      new JButton(
        "FRIEND REQUESTS"
      )


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


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


    private var selectedPerson =
      ""


    private var currentChat =
      ""


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

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

    private val mutedChats =
      mutable.Set[String]()

    private val typingTimer =
      new javax.swing.Timer(900, new ActionListener {
        override def actionPerformed(e: ActionEvent): Unit = {
          if (currentChat != null && currentChat.nonEmpty) {
            client.send("TYPING|" + Util.encode(currentChat) + "|0")
          }
        }
      })


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

    def show(): Unit = {

      Util.setupFont()

      updateFriendButtons("NONE")


      frame.setDefaultCloseOperation(
        WindowConstants.DISPOSE_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 userText =
        new JLabel(
          "Logged in: " +
            client.username +
            "   "
        )


      userText.setForeground(
        Color.WHITE
      )


      top.add(
        title,
        BorderLayout.WEST
      )


      val topRight =
        new JPanel(
          new BorderLayout(8, 0)
        )

      topRight.setOpaque(false)
      topRight.add(onlineSummary, BorderLayout.CENTER)
      topRight.add(userText, BorderLayout.WEST)
      topRight.add(logoutButton, BorderLayout.EAST)

      onlineSummary.setForeground(Color.WHITE)
      logoutButton.setFocusable(false)

      top.add(
        topRight,
        BorderLayout.EAST
      )


      root.add(
        top,
        BorderLayout.NORTH
      )


      // ======================================================
      // LEFT
      // ======================================================

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


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


      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
      // ------------------------------------------------------

      peopleList.setSelectionMode(
        ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
      )


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


      peoplePanel.add(
        new JLabel(
          " PEOPLE"
        ),
        BorderLayout.NORTH
      )


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


      // ------------------------------------------------------
      // CHATS
      // ------------------------------------------------------

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

      friendsPanel.add(
        new JLabel(
          " FRIENDS"
        ),
        BorderLayout.NORTH
      )

      friendsPanel.add(
        new JScrollPane(
          friendsList
        ),
        BorderLayout.CENTER
      )


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


      chatPanel.add(
        new JLabel(
          " MY CHATS"
        ),
        BorderLayout.NORTH
      )


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


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


      leftCenter.add(
        peoplePanel
      )


      leftCenter.add(
        friendsPanel
      )


      leftCenter.add(
        chatPanel
      )


      left.add(
        leftCenter,
        BorderLayout.CENTER
      )


      val leftActions =
        new JPanel(
          new GridLayout(
            1,
            3,
            4,
            4
          )
        )

      leftActions.add(
        groupButton
      )

      leftActions.add(
        friendsButton
      )

      leftActions.add(
        requestsButton
      )

      left.add(
        leftActions,
        BorderLayout.SOUTH
      )


      root.add(
        left,
        BorderLayout.WEST
      )


      // ======================================================
      // CENTER
      // ======================================================

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


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


      val chatHeader =
        new JPanel(
          new BorderLayout(5, 2)
        )

      chatHeader.add(
        chatTitle,
        BorderLayout.WEST
      )

      chatHeader.add(
        typingLabel,
        BorderLayout.CENTER
      )

      center.add(
        chatHeader,
        BorderLayout.NORTH
      )


      chatArea.setEditable(
        false
      )


      chatArea.setLineWrap(
        true
      )


      chatArea.setWrapStyleWord(
        true
      )


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


      val messageScroll =
        new JScrollPane(
          chatArea
        )


      center.add(
        messageScroll,
        BorderLayout.CENTER
      )


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


      sendPanel.add(
        messageField,
        BorderLayout.CENTER
      )


      val sendTools =
        new JPanel(
          new GridLayout(1, 3, 4, 4)
        )

      sendTools.add(sendButton)
      sendTools.add(exportButton)
      sendTools.add(muteButton)

      sendPanel.add(
        sendTools,
        BorderLayout.EAST
      )

      center.add(
        sendPanel,
        BorderLayout.SOUTH
      )


      root.add(
        center,
        BorderLayout.CENTER
      )


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

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


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


      right.add(
        new JLabel(
          " PROFILE"
        ),
        BorderLayout.NORTH
      )


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


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


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


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


      names.add(
        profileName
      )


      names.add(
        profileStatus
      )


      profileText.add(
        names,
        BorderLayout.NORTH
      )


      profileBio.setEditable(
        false
      )


      profileBio.setLineWrap(
        true
      )


      profileBio.setWrapStyleWord(
        true
      )


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


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


      profileCenter.add(
        profileImage,
        BorderLayout.NORTH
      )


      profileCenter.add(
        profileText,
        BorderLayout.CENTER
      )


      right.add(
        profileCenter,
        BorderLayout.CENTER
      )


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

      profileButtons.add(openChatButton)
      profileButtons.add(friendButton)
      profileButtons.add(acceptButton)
      profileButtons.add(rejectButton)
      profileButtons.add(removeFriendButton)
      profileButtons.add(blockButton)
      profileButtons.add(editButton)


      right.add(
        profileButtons,
        BorderLayout.SOUTH
      )


      root.add(
        right,
        BorderLayout.EAST
      )


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

      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
      // ======================================================

      peopleList.addMouseListener(
        new MouseAdapter {

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

            if (
              e.getClickCount == 2
            ) {

              val person =
                peopleList
                  .getSelectedValue


              if (
                person != null
              ) {

                openDirectChat(
                  person.username
                )
              }
            }
          }
        }
      )


      // ======================================================
      // FRIEND ACTIONS
      // ======================================================

      friendButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (selectedPerson.nonEmpty) {
              client.send(
                "FRIEND_REQUEST|" + Util.encode(selectedPerson)
              )
            }
          }
        }
      )

      acceptButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (selectedPerson.nonEmpty) {
              client.send(
                "FRIEND_ACCEPT|" + Util.encode(selectedPerson)
              )
            }
          }
        }
      )

      rejectButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (selectedPerson.nonEmpty) {
              client.send(
                "FRIEND_REJECT|" + Util.encode(selectedPerson)
              )
            }
          }
        }
      )

      removeFriendButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (selectedPerson.nonEmpty) {
              client.send(
                "FRIEND_REMOVE|" + Util.encode(selectedPerson)
              )
            }
          }
        }
      )

      friendsButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            refreshFriends()
          }
        }
      )

      requestsButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            client.send("FRIEND_REQUESTS")
          }
        }
      )

      friendsList.addListSelectionListener(
        _ => {
          val person = friendsList.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))
          }
        }
      )

      friendsList.addMouseListener(
        new MouseAdapter {
          override def mouseClicked(e: MouseEvent): Unit = {
            if (e.getClickCount == 2) {
              val person = friendsList.getSelectedValue
              if (person != null) openDirectChat(person.username)
            }
          }
        }
      )


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

      openChatButton.addActionListener(
        new ActionListener {

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

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

              openDirectChat(
                selectedPerson
              )
            }
          }
        }
      )


      // ======================================================
      // CHAT LIST
      // ======================================================

      chatList.addListSelectionListener(
        _ => {

          val chat =
            chatList
              .getSelectedValue


          if (
            chat != null
          ) {

            currentChat =
              chat.id

            unreadMap.remove(chat.id)
            val cleared = chat.copy(unread = 0)
            chatMap.put(chat.id, cleared)
            replaceChatItem(cleared)
            typingLabel.setText("")
            typingTimer.stop()
            client.send("TYPING|" + Util.encode(chat.id) + "|0")
            muteButton.setText(if (mutedChats.contains(chat.id)) "UNMUTE CHAT" else "MUTE CHAT")

            chatTitle.setText(
              chat.title
            )


            chatArea.setText(
              "Loading messages..."
            )


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


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

      sendButton.addActionListener(
        new ActionListener {

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

            sendMessage()
          }
        }
      )


      messageField.addActionListener(
        new ActionListener {

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

            sendMessage()
          }
        }
      )


      // ======================================================
      // EDIT
      // ======================================================

      editButton.addActionListener(
        new ActionListener {

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

            editProfile()
          }
        }
      )

      blockButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (selectedPerson.nonEmpty) {
              if (blockButton.getText == "UNBLOCK USER") {
                client.send("UNBLOCK_USER|" + Util.encode(selectedPerson))
              } else {
                val ok = JOptionPane.showConfirmDialog(
                  frame,
                  "Block " + selectedPerson + "? Friend requests and direct chat will be stopped.",
                  APP_NAME,
                  JOptionPane.YES_NO_OPTION,
                  JOptionPane.WARNING_MESSAGE
                )
                if (ok == JOptionPane.YES_OPTION) {
                  client.send("BLOCK_USER|" + Util.encode(selectedPerson))
                }
              }
            }
          }
        }
      )

      logoutButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            val ok = JOptionPane.showConfirmDialog(
              frame,
              "Logout from " + client.username + "?",
              APP_NAME,
              JOptionPane.YES_NO_OPTION
            )
            if (ok == JOptionPane.YES_OPTION) {
              client.send("LOGOUT")
              client.close()
              frame.dispose()
              new LoginFrame().show()
            }
          }
        }
      )

      exportButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            exportCurrentChat()
          }
        }
      )

      muteButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (currentChat != null && currentChat.nonEmpty) {
              if (mutedChats.contains(currentChat)) {
                mutedChats.remove(currentChat)
                muteButton.setText("MUTE CHAT")
              } else {
                mutedChats.add(currentChat)
                muteButton.setText("UNMUTE CHAT")
              }
            }
          }
        }
      )

      messageField.getDocument.addDocumentListener(
        new DocumentListener {
          private def changed(): Unit = {
            if (currentChat != null && currentChat.nonEmpty && messageField.getText.nonEmpty) {
              typingTimer.restart()
              client.send(
                "TYPING|" + Util.encode(currentChat) + "|1"
              )
            }
          }
          override def insertUpdate(e: DocumentEvent): Unit = changed()
          override def removeUpdate(e: DocumentEvent): Unit = changed()
          override def changedUpdate(e: DocumentEvent): Unit = changed()
        }
      )


      frame.setContentPane(
        root
      )


      frame.setVisible(
        true
      )

      refreshFriends()
      client.send("FRIEND_REQUESTS")
      searchPeople()
    }


    private def replaceChatItem(item: ChatItem): Unit = {
      chatMap.put(item.id, item)
      var i = 0
      while (i < chatModel.size()) {
        val old = chatModel.getElementAt(i)
        if (old.id == item.id) {
          chatModel.setElementAt(item, i)
          return
        }
        i += 1
      }
      chatModel.addElement(item)
    }


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

    private def searchPeople(): Unit = {

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


    // ========================================================
    // REFRESH FRIENDS
    // ========================================================

    private def refreshFriends(): Unit = {
      client.send(
        "FRIENDS_LIST"
      )
    }


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

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

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


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

    private def sendMessage(): Unit = {

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

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

        return
      }


      val message =
        messageField
          .getText
          .trim


      if (
        message.isEmpty
      ) {

        return
      }


      typingTimer.stop()
      client.send("TYPING|" + Util.encode(currentChat) + "|0")

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


      messageField.setText(
        ""
      )
    }


    private def exportCurrentChat(): Unit = {
      if (currentChat == null || currentChat.isEmpty) {
        JOptionPane.showMessageDialog(
          frame,
          "First open a chat.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )
        return
      }

      val chooser = new JFileChooser()
      chooser.setDialogTitle("EXPORT CHAT")
      chooser.setSelectedFile(new java.io.File("YadneshChat_" + System.currentTimeMillis() + ".txt"))

      if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
        try {
          Files.write(
            chooser.getSelectedFile.toPath,
            chatArea.getText.getBytes(StandardCharsets.UTF_8)
          )
          JOptionPane.showMessageDialog(
            frame,
            "Chat exported successfully.",
            APP_NAME,
            JOptionPane.INFORMATION_MESSAGE
          )
        } catch {
          case _: Throwable =>
            JOptionPane.showMessageDialog(
              frame,
              "Could not export chat.",
              APP_NAME,
              JOptionPane.ERROR_MESSAGE
            )
        }
      }
    }


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

    private def createGroup(): Unit = {

      val selected =
        peopleList
          .getSelectedValuesList()


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

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

        return
      }


      val name =
        JOptionPane.showInputDialog(
          frame,
          "Group name:",
          "CREATE GROUP",
          JOptionPane.QUESTION_MESSAGE
        )


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

        return
      }


      val request =
        new StringBuilder()


      request.append(
        "CREATE_GROUP|"
      )


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


      request.append(
        "|" +
          selected.size()
      )


      var i =
        0


      while (
        i < selected.size()
      ) {

        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(
        700,
        540
      )


      dialog.setLocationRelativeTo(
        frame
      )


      val bio =
        new JTextField(
          client.ownBio
        )


      val choose =
        new JButton(
          "CHOOSE IMAGE"
        )


      val paste =
        new JButton(
          "PASTE IMAGE"
        )


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


      var avatar =
        client.ownAvatar


      val save =
        new JButton(
          "SAVE PROFILE"
        )


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


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


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


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


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


      form.add(
        bio
      )


      form.add(
        new JLabel(
          "Profile Image:"
        )
      )


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


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


      imageButtons.add(
        choose
      )


      imageButtons.add(
        paste
      )


      imagePanel.add(
        imageButtons,
        BorderLayout.WEST
      )


      imagePanel.add(
        imageLabel,
        BorderLayout.CENTER
      )


      form.add(
        imagePanel
      )


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


      form.add(
        save
      )


      choose.addActionListener(
        new ActionListener {

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

            val chooser =
              new JFileChooser()


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

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


              if (
                encoded.nonEmpty
              ) {

                avatar =
                  encoded


                imageLabel.setText(
                  "New image selected ?"
                )

              } else {

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


      paste.addActionListener(
        new ActionListener {

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

            val encoded =
              Util.clipboardImageBase64()


            if (
              encoded.nonEmpty
            ) {

              avatar =
                encoded


              imageLabel.setText(
                "Pasted image saved ?"
              )

            } else {

              JOptionPane.showMessageDialog(
                dialog,
                "Clipboard does not contain an image.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )
            }
          }
        }
      )


      save.addActionListener(
        new ActionListener {

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

            client.ownBio =
              bio.getText.trim


            client.ownAvatar =
              avatar


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


            dialog.dispose()
          }
        }
      )


      dialog.setContentPane(
        form
      )


      dialog.setVisible(
        true
      )
    }


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

    def handleServerEvent(
        line: String
    ): Unit = {

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


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


      p(0) match {

        // ----------------------------------------------------
        // SEARCH RESULTS
        // ----------------------------------------------------

        case "SEARCH_RESULT" =>

          peopleModel.clear()


          if (
            p.length >= 2
          ) {

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


            var index =
              2


            var i =
              0


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

              val name =
                Util.decode(
                  p(index)
                )


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


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

              val relation =
                if (index + 3 < p.length) p(index + 3) else "NONE"

              peopleModel.addElement(
                PersonItem(
                  name,
                  bio,
                  online,
                  relation
                )
              )


              index +=
                (if (index + 3 < p.length) 4 else 3)


              i +=
                1
            }
          }


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

        case "PROFILE_DATA" =>

          if (
            p.length >= 5
          ) {

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


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


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


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

            val relation =
              if (p.length >= 6) p(5) else "NONE"

            updateFriendButtons(relation)


            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
                }
              )


              displayAvatar(
                avatar
              )
            }
          }


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

        case "PROFILE_SAVED" =>

          JOptionPane.showMessageDialog(
            frame,
            "Profile saved permanently on the host server.",
            APP_NAME,
            JOptionPane.INFORMATION_MESSAGE
          )


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


          searchPeople()


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

        case "PROFILE_CHANGED" =>

          if (
            p.length >= 2
          ) {

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


            if (
              name.equalsIgnoreCase(
                selectedPerson
              )
            ) {

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


            searchPeople()
            refreshFriends()
          }


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

        case "CHAT" =>

          if (
            p.length >= 4
          ) {

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


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


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


            if (
              !chatMap.contains(
                id
              )
            ) {

              val entry =
                ChatItem(
                  id,
                  title,
                  group,
                  unreadMap.getOrElse(id, 0)
                )


              chatMap.put(
                id,
                entry
              )


              chatModel.addElement(
                entry
              )
            }
          }


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

        case "HISTORY" =>

          if (
            p.length >= 3
          ) {

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


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


            if (
              id ==
                currentChat
            ) {

              val out =
                new StringBuilder()


              var index =
                3


              var i =
                0


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

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


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


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


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


                index +=
                  3


                i +=
                  1
              }


              chatArea.setText(
                out.toString
              )


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


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

        case "MESSAGE" =>

          if (
            p.length >= 5
          ) {

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


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


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


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


            if (
              id ==
                currentChat
            ) {

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


              chatArea.setCaretPosition(
                chatArea.getDocument
                  .getLength
              )
            } else {
              val next = unreadMap.getOrElse(id, 0) + 1
              unreadMap.put(id, next)
              chatMap.get(id).foreach { old =>
                replaceChatItem(old.copy(unread = next))
              }
              if (!mutedChats.contains(id)) {
                Toolkit.getDefaultToolkit.beep()
              }
            }
          }


        // ----------------------------------------------------
        // TYPING
        // ----------------------------------------------------

        case "TYPING" =>

          if (p.length >= 4) {
            val id = Util.decode(p(1))
            val name = Util.decode(p(2))
            val state = p(3) == "1"
            if (id == currentChat && !name.equalsIgnoreCase(client.username)) {
              typingLabel.setText(if (state) name + " is typing..." else "")
            }
          }


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

        case "PRESENCE" =>

          if (
            p.length >= 3
          ) {

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


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


            var i =
              0


            while (
              i < peopleModel.size()
            ) {

              val old =
                peopleModel
                  .getElementAt(
                    i
                  )


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

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


              i +=
                1
            }


            var j = 0
            while (j < friendsModel.size()) {
              val oldFriend = friendsModel.getElementAt(j)
              if (oldFriend.username.equalsIgnoreCase(name)) {
                friendsModel.setElementAt(oldFriend.copy(online = online), j)
              }
              j += 1
            }

            if (
              selectedPerson
                .equalsIgnoreCase(
                  name
                )
            ) {

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


        // ----------------------------------------------------
        // FRIENDS LIST
        // ----------------------------------------------------

        case "FRIENDS_LIST" =>

          friendsModel.clear()

          if (p.length >= 2) {
            val count = try p(1).toInt catch { case _: Throwable => 0 }
            var onlineCount = 0
            var index = 2
            var i = 0
            while (i < count && index + 2 < p.length) {
              val name = Util.decode(p(index))
              val online = p(index + 1) == "1"
              if (online) onlineCount += 1
              val bio = Util.decode(p(index + 2))
              friendsModel.addElement(PersonItem(name, bio, online, "FRIEND"))
              index += 3
              i += 1
            }
            onlineSummary.setText("  FRIENDS ONLINE: " + onlineCount + "/" + count + "  ")
          }

        // ----------------------------------------------------
        // FRIEND REQUESTS
        // ----------------------------------------------------

        case "FRIEND_REQUESTS" =>

          requestModel.clear()

          if (p.length >= 2) {
            val count = try p(1).toInt catch { case _: Throwable => 0 }
            var index = 2
            var i = 0
            while (i < count && index + 2 < p.length) {
              val name = Util.decode(p(index))
              val online = p(index + 1) == "1"
              val bio = Util.decode(p(index + 2))
              requestModel.addElement(PersonItem(name, bio, online, "INCOMING"))
              index += 3
              i += 1
            }

            requestsButton.setText(
              if (count > 0) "FRIEND REQUESTS (" + count + ")" else "FRIEND REQUESTS"
            )

            if (count > 0) {
              val choices = new Array[String](requestModel.size())
              var k = 0
              while (k < requestModel.size()) {
                choices(k) = requestModel.getElementAt(k).username
                k += 1
              }

              val choiceObjects = choices.map(_.asInstanceOf[Object])

              val selected = JOptionPane.showInputDialog(
                frame,
                "Pending friend requests. Select a person:",
                "FRIEND REQUESTS",
                JOptionPane.QUESTION_MESSAGE,
                null,
                choiceObjects,
                choices(0)
              )

              if (selected != null) {
                val action = JOptionPane.showOptionDialog(
                  frame,
                  "Friend request from " + selected,
                  "FRIEND REQUEST",
                  JOptionPane.DEFAULT_OPTION,
                  JOptionPane.QUESTION_MESSAGE,
                  null,
                  Array[AnyRef]("ACCEPT", "REJECT", "CANCEL"),
                  "ACCEPT"
                )
                if (action == 0) {
                  client.send("FRIEND_ACCEPT|" + Util.encode(selected.toString))
                } else if (action == 1) {
                  client.send("FRIEND_REJECT|" + Util.encode(selected.toString))
                }
              }
            }
          }

        // ----------------------------------------------------
        // FRIEND EVENTS
        // ----------------------------------------------------

        case "FRIEND_REQUEST_SENT" =>
          searchPeople()

        case "FRIEND_REQUEST_RECEIVED" =>
          val from = if (p.length >= 2) Util.decode(p(1)) else "Someone"
          JOptionPane.showMessageDialog(
            frame,
            from + " sent you a friend request. Open FRIEND REQUESTS to accept or reject.",
            APP_NAME,
            JOptionPane.INFORMATION_MESSAGE
          )
          client.send("FRIEND_REQUESTS")

        case "FRIEND_ACCEPTED" =>
          val accepted = if (p.length >= 2) Util.decode(p(1)) else "Friend"
          JOptionPane.showMessageDialog(
            frame,
            "You are now friends with " + accepted + ". The private chat is ready.",
            APP_NAME,
            JOptionPane.INFORMATION_MESSAGE
          )
          refreshFriends()
          searchPeople()

        case "FRIEND_REQUEST_REJECTED" =>
          refreshFriends()
          searchPeople()

        case "FRIEND_REMOVED" =>
          refreshFriends()
          searchPeople()

        case "BLOCK_STATE" =>
          val blockedName = if (p.length >= 2) Util.decode(p(1)) else "User"
          val blocked = p.length >= 3 && p(2) == "1"
          blockButton.setText(if (blocked) "UNBLOCK USER" else "BLOCK USER")
          if (blocked) {
            JOptionPane.showMessageDialog(frame, blockedName + " is blocked.", APP_NAME, JOptionPane.INFORMATION_MESSAGE)
          }
          refreshFriends()
          searchPeople()
          if (selectedPerson.equalsIgnoreCase(blockedName)) {
            client.send("PROFILE|" + Util.encode(selectedPerson))
          }

        case "BLOCKED_BY" =>
          val blocker = if (p.length >= 2) Util.decode(p(1)) else "A user"
          JOptionPane.showMessageDialog(frame, blocker + " blocked you.", APP_NAME, JOptionPane.WARNING_MESSAGE)
          refreshFriends()
          searchPeople()

        case "UNBLOCKED_BY" =>
          refreshFriends()
          searchPeople()

        case "CLIENT_DISCONNECTED" =>
          onlineSummary.setText("  CONNECTION LOST  ")
          typingLabel.setText("")
          JOptionPane.showMessageDialog(
            frame,
            "The connection to the host server was lost. Restart/login again to reconnect.",
            APP_NAME,
            JOptionPane.WARNING_MESSAGE
          )


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

        case "GROUP_CREATED" =>

          if (
            p.length >= 2
          ) {

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


        case _ =>
      }
    }


    // ========================================================
    // FRIEND BUTTON STATE
    // ========================================================

    private def updateFriendButtons(
        relation: String
    ): Unit = {

      friendButton.setEnabled(
        relation == "NONE"
      )

      acceptButton.setEnabled(
        relation == "INCOMING"
      )

      rejectButton.setEnabled(
        relation == "INCOMING"
      )

      removeFriendButton.setEnabled(
        relation == "FRIEND"
      )

      openChatButton.setEnabled(
        relation == "FRIEND"
      )

      blockButton.setEnabled(
        selectedPerson.nonEmpty && relation != ""
      )

      blockButton.setText(
        if (relation == "BLOCKED") "UNBLOCK USER" else "BLOCK USER"
      )
    }


    // ========================================================
    // DISPLAY AVATAR
    // ========================================================

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

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

        profileImage.setIcon(
          null
        )


        profileImage.setText(
          "No Photo"
        )


      } else {

        val image =
          Util.base64ToImage(
            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.setupFont()


      val frame =
        new JFrame(
          APP_NAME
        )


      frame.setDefaultCloseOperation(
        WindowConstants.DISPOSE_ON_CLOSE
      )


      frame.setSize(
        720,
        520
      )


      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'>PERMANENT ACCOUNT ? WIFI LIVE CHAT</font>" +
            "</center>" +
            "</html>",
          SwingConstants.CENTER
        )


      root.add(
        title,
        BorderLayout.NORTH
      )


      val info =
        new JTextArea(
          "FEATURES\n\n" +
            "? Permanent user accounts\n" +
            "? Same account after restarting app\n" +
            "? Same account available from another PC through the same server\n" +
            "? PC to PC live Wi-Fi chat\n" +
            "? 2-person direct chat\n" +
            "? Multiple-person group chat\n" +
            "? Username search\n" +
            "? Online / offline status\n" +
            "? Profile photo\n" +
            "? CHOOSE IMAGE\n" +
            "? PASTE IMAGE FROM CLIPBOARD\n" +
            "? Public profile bio\n" +
            "? Forgot Password recovery\n" +
            "? Live typing indicator\n" +
            "? Unread message counters\n" +
            "? Block / unblock users\n" +
            "? Logout + connection heartbeat\n" +
            "? Export current chat to TXT\n\n" +
            "Account files are stored on the HOST SERVER PC."
        )


      info.setEditable(
        false
      )


      info.setLineWrap(
        true
      )


      info.setWrapStyleWord(
        true
      )


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


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


      start.setPreferredSize(
        new Dimension(
          330,
          55
        )
      )


      root.add(
        start,
        BorderLayout.SOUTH
      )


      start.addActionListener(
        new ActionListener {

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

            frame.dispose()


            val login =
              new LoginFrame()


            login.show()
          }
        }
      )


      frame.setContentPane(
        root
      )


      frame.setVisible(
        true
      )
    }
  }


  // ==========================================================
  // LAUNCHER
  // ==========================================================

  def startApp(): Unit = {

    SwingUtilities.invokeLater(
      new Runnable {

        override def run(): Unit = {

          try {

            UIManager.setLookAndFeel(
              UIManager
                .getSystemLookAndFeelClassName
            )

          } catch {

            case _: Throwable =>
          }


          StartScreen.show()
        }
      }
    )
  }
}

import javax.swing._
import java.awt._
import java.awt.event._
import java.awt.image.BufferedImage
import java.awt.image.DataBufferInt
import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable

/*
 * OLD HOUSE 3D - improved standalone Scala/Swing prototype.
 * Features: triangle rasterizer + depth buffer, near-plane clipping,
 * hinged opening doors, stairs with changing player height, upstairs
 * gallery, WASD movement, mouse look, flashlight, key, wardrobe hiding,
 * noise attraction, enemy patrol/chase, and 5-day loop.
 *
 * This is an original low-poly prototype, not the original Granny game.
 * It uses no third-party game-engine libraries, but it is not a
 * commercial-quality full 3D engine.
 */
object OldHouse3DImproved {
  case class V3(x: Double, y: Double, z: Double)
  case class CV(x: Double, y: Double, z: Double)
  case class Tri(a: V3, b: V3, c: V3, color: Color)
  case class Box(x: Double, y: Double, z: Double,
                 sx: Double, sy: Double, sz: Double,
                 color: Color, solid: Boolean)

  final class Door(val x: Double, val z: Double, val width: Double,
                   val plane: String, val hingeSide: Double,
                   val yBase: Double, val height: Double,
                   val locked: Boolean, val isExit: Boolean,
                   val name: String) {
    var angle = 0.0
    var target = 0.0
  }

  val SW = 1000
  val SH = 620
  val NEAR = 0.12
  val FOCAL = 760.0
  val WALL = new Color(86, 75, 64)
  val WALL_ALT = new Color(110, 91, 72)
  val FLOOR = new Color(76, 60, 46)
  val WOOD = new Color(91, 57, 36)
  val DARK_WOOD = new Color(46, 31, 24)

  val triangles = ArrayBuffer[Tri]()
  val solids = ArrayBuffer[Box]()
  val doors = ArrayBuffer[Door]()
  val keys = mutable.HashSet[Int]()

  var px = 3.2
  var pz = 4.15
  var yaw = math.Pi
  var pitch = 0.0
  var onUpperFloor = false
  var wakeProgress = 0.0
  var started = false
  var gameOver = false
  var escaped = false
  var hidden = false
  var hidePlace = ""
  var hasKey = false
  var hasBomb = false
  var bombThrown = false
  var bombX = 0.0
  var bombZ = 0.0
  var bombY = 0.25
  var bombDX = 0.0
  var bombDZ = 0.0
  var bombFuse = 0.0
  var bombBlastTimer = 0.0
  var enemyStunnedTimer = 0.0
  var secretUnlocked = false
  var secretLootTaken = false
  var flashlight = true
  var showMap = false
  var day = 1

  val keyX = 9.35
  val keyZ = 3.35
  val wardrobeX = 1.25
  val wardrobeZ = 4.7
  val bedX = 2.3
  val bedZ = 2.1
  val vaseX = 4.65
  val vaseZ = 2.35
  // Discoverable shelf and the hidden room behind its secret panel.
  val clueShelfX = 3.55
  val clueShelfZ = 10.55
  val secretShelfX = 3.15
  val secretShelfZ = 14.65
  val bathroomDoorX = 18.65
  val bathroomDoorZ = 6.0
  val ladderX = 14.80
  val ladderZ = 14.00
  var vaseY = 0.9
  var vaseVel = 0.0
  var vaseFalling = false
  var vaseBroken = false
  var noiseX = vaseX
  var noiseZ = vaseZ
  var noiseTimer = 0.0

  // Original pursuer character: not a copied Granny model.
  var enemyX = 9.4
  var enemyZ = 8.3
  var enemyUpper = false
  var enemyTargetX = enemyX
  var enemyTargetZ = enemyZ
  var aiTimer = 0.0
  var patrolIndex = 0
  val patrol = Array((9.4, 8.3), (10.3, 10.0), (9.2, 14.0),
                     (15.8, 8.0), (19.0, 9.0))

  var message = "DAY 1 - PRESS ENTER TO WAKE UP"
  var messageTimer = 5.0
  var lastMouseX = -1
  var lastMouseY = -1
  var gameTimer: Timer = _
  var frame: JFrame = _
  var panel: GamePanel = _
  var image: BufferedImage = _
  var pixels: Array[Int] = _
  var zBuffer: Array[Double] = _

  def clamp(v: Double, lo: Double, hi: Double): Double =
    math.max(lo, math.min(hi, v))

  def dist(x1: Double, z1: Double, x2: Double, z2: Double): Double = {
    val dx = x1 - x2
    val dz = z1 - z2
    math.sqrt(dx * dx + dz * dz)
  }

  def shade(c: Color, f0: Double): Color = {
    val f = clamp(f0, 0.0, 1.35)
    new Color(
      clamp(c.getRed * f, 0, 255).toInt,
      clamp(c.getGreen * f, 0, 255).toInt,
      clamp(c.getBlue * f, 0, 255).toInt
    )
  }

  // -------------------------- Geometry --------------------------

  def addBox(x: Double, y: Double, z: Double,
             sx: Double, sy: Double, sz: Double,
             color: Color, solid: Boolean = true): Unit = {
    val b = Box(x, y, z, sx, sy, sz, color, solid)
    triangles ++= boxTriangles(b)
    if (solid) solids += b
  }

  def boxTriangles(b: Box): ArrayBuffer[Tri] = {
    val x0 = b.x - b.sx / 2; val x1 = b.x + b.sx / 2
    val y0 = b.y - b.sy / 2; val y1 = b.y + b.sy / 2
    val z0 = b.z - b.sz / 2; val z1 = b.z + b.sz / 2
    val v = Array(
      V3(x0,y0,z0), V3(x1,y0,z0), V3(x1,y1,z0), V3(x0,y1,z0),
      V3(x0,y0,z1), V3(x1,y0,z1), V3(x1,y1,z1), V3(x0,y1,z1)
    )
    val faces = Array(
      Array(0,3,2,1), Array(4,5,6,7), Array(0,4,7,3),
      Array(1,2,6,5), Array(3,7,6,2), Array(0,1,5,4)
    )
    val factors = Array(0.68, 0.95, 0.58, 0.78, 1.22, 0.40)
    val out = ArrayBuffer[Tri]()
    for (i <- faces.indices) {
      val f = faces(i)
      val c = shade(b.color, factors(i))
      out += Tri(v(f(0)), v(f(1)), v(f(2)), c)
      out += Tri(v(f(0)), v(f(2)), v(f(3)), c)
    }
    out
  }

  def addWallX(x: Double, z0: Double, z1: Double,
               gaps: Seq[(Double, Double)], yBase: Double = 0.0,
               height: Double = 2.8, color: Color = WALL): Unit = {
    var cursor = z0
    for ((a,b) <- gaps.sortBy(_._1)) {
      if (a > cursor + 0.001)
        addBox(x, yBase + height/2, (cursor+a)/2,
          0.22, height, a-cursor, color, true)
      cursor = math.max(cursor,b)
    }
    if (cursor < z1 - 0.001)
      addBox(x, yBase + height/2, (cursor+z1)/2,
        0.22, height, z1-cursor, color, true)
  }

  def addWallZ(z: Double, x0: Double, x1: Double,
               gaps: Seq[(Double, Double)], yBase: Double = 0.0,
               height: Double = 2.8, color: Color = WALL): Unit = {
    var cursor = x0
    for ((a,b) <- gaps.sortBy(_._1)) {
      if (a > cursor + 0.001)
        addBox((cursor+a)/2, yBase + height/2, z,
          a-cursor, height, 0.22, color, true)
      cursor = math.max(cursor,b)
    }
    if (cursor < x1 - 0.001)
      addBox((cursor+x1)/2, yBase + height/2, z,
        x1-cursor, height, 0.22, color, true)
  }

  def addDoor(x: Double, z: Double, width: Double, plane: String,
              hingeSide: Double = 1.0, yBase: Double = 0.0,
              height: Double = 2.65, locked: Boolean = false,
              isExit: Boolean = false, name: String = "Door"): Unit = {
    doors += new Door(x,z,width,plane,hingeSide,yBase,height,locked,isExit,name)
  }

  def makeDoorTriangles(d: Door): ArrayBuffer[Tri] = {
    val c = math.cos(d.angle)
    val s = math.sin(d.angle)
    // Local width and normal before rotating around the hinge.
    val (baseUx, baseUz, baseNx, baseNz, pivotX, pivotZ) =
      if (d.plane == "X") {
        (0.0, d.hingeSide, d.hingeSide, 0.0,
         d.x, d.z - d.hingeSide*d.width/2)
      } else {
        (d.hingeSide, 0.0, 0.0, 1.0,
         d.x - d.hingeSide*d.width/2, d.z)
      }
    def rotateX(ux: Double, uz: Double): Double = ux*c + uz*s
    def rotateZ(ux: Double, uz: Double): Double = uz*c - ux*s
    val ux = rotateX(baseUx, baseUz)
    val uz = rotateZ(baseUx, baseUz)
    val nx = rotateX(baseNx, baseNz)
    val nz = rotateZ(baseNx, baseNz)
    val thick = 0.12
    def point(u: Double, v: Double, y: Double): V3 =
      V3(pivotX + ux*u + nx*v, y, pivotZ + uz*u + nz*v)
    val v = Array(
      point(0,-thick/2,d.yBase), point(d.width,-thick/2,d.yBase),
      point(d.width,-thick/2,d.yBase+d.height), point(0,-thick/2,d.yBase+d.height),
      point(0,thick/2,d.yBase), point(d.width,thick/2,d.yBase),
      point(d.width,thick/2,d.yBase+d.height), point(0,thick/2,d.yBase+d.height)
    )
    val faces = Array(
      Array(0,3,2,1), Array(4,5,6,7), Array(0,4,7,3),
      Array(1,2,6,5), Array(3,7,6,2), Array(0,1,5,4)
    )
    val out = ArrayBuffer[Tri]()
    val factors = Array(0.72, 0.88, 0.56, 0.92, 1.12, 0.42)
    for (i <- faces.indices) {
      val f = faces(i)
      val col = shade(if (d.isExit) new Color(75,40,27) else new Color(94,59,37), factors(i))
      out += Tri(v(f(0)),v(f(1)),v(f(2)),col)
      out += Tri(v(f(0)),v(f(2)),v(f(3)),col)
    }
    // Small handle on the front side.
    val handle = Box(
      pivotX + ux*d.width*0.82 + nx*0.09,
      d.yBase + d.height*0.48,
      pivotZ + uz*d.width*0.82 + nz*0.09,
      0.08, 0.08, 0.08, new Color(211,174,73), false
    )
    out ++= boxTriangles(handle)
    out
  }

  def buildHouse(): Unit = {
    triangles.clear(); solids.clear(); doors.clear()

    // Main ground-floor slab.
    addBox(12.0,-0.16,9.0,24.0,0.32,18.0,FLOOR,false)

    // Exterior walls, with a front exit opening.
    addBox(0.12,1.4,9.0,0.24,2.8,18.0,WALL,true)
    addBox(23.88,1.4,9.0,0.24,2.8,18.0,WALL,true)
    addWallZ(0.12,0.12,23.88,Seq(),0.0,2.8,WALL_ALT)
    addWallZ(17.88,0.12,23.88,Seq((19.7,21.3)),0.0,2.8,WALL_ALT)

    // Ground-floor partitions. Door openings are real gaps in the wall.
    addWallX(6.0,0.25,17.75,Seq((2.4,3.6),(8.2,9.4)))
    addWallX(12.0,0.25,17.75,Seq((3.0,4.2),(8.0,9.2),(14.6,15.8)))
    // The broad opening to the right leads into the bathroom wing.
    addWallZ(6.0,0.25,23.75,Seq((3.5,4.8),(8.0,9.2),(14.3,17.3),(18.0,19.3)))
    // The x=4.0..5.2 gap is hidden behind a hinged secret panel.
    addWallZ(12.0,0.25,23.75,Seq((4.0,5.2),(8.5,9.7),(15.5,16.7),(19.9,21.3)))

    addDoor(6.0,3.0,1.2,"X",1.0,0.0,2.65,false,false,"Bedroom door")
    addDoor(6.0,8.8,1.2,"X",-1.0,0.0,2.65,false,false,"Hall door")
    addDoor(12.0,3.6,1.2,"X",-1.0,0.0,2.65,false,false,"Kitchen door")
    addDoor(12.0,8.6,1.2,"X",1.0,0.0,2.65,false,false,"Stair hall door")
    addDoor(12.0,15.2,1.2,"X",-1.0,0.0,2.65,false,false,"Back room door")
    addDoor(4.15,6.0,1.3,"Z",1.0,0.0,2.65,false,false,"Room door")
    addDoor(8.6,6.0,1.2,"Z",-1.0,0.0,2.65,false,false,"Hall door")
    addDoor(9.1,12.0,1.2,"Z",-1.0,0.0,2.65,false,false,"Back hall door")
    addDoor(16.1,12.0,1.2,"Z",1.0,0.0,2.65,false,false,"Back room door")
    addDoor(20.6,12.0,1.2,"Z",-1.0,0.0,2.65,false,false,"Exit hall door")
    addDoor(20.5,17.88,1.6,"Z",1.0,0.0,2.65,true,true,"Locked exit")
    addDoor(bathroomDoorX,bathroomDoorZ,1.3,"Z",1.0,0.0,2.65,false,false,"Bathroom door")
    addDoor(4.6,12.0,1.2,"Z",1.0,0.0,2.65,false,false,"Hidden shelf panel")

    // Bedroom furniture.
    addBox(2.3,0.23,2.1,2.25,0.34,2.9,DARK_WOOD,true)
    addBox(2.3,0.48,2.1,2.05,0.18,2.68,new Color(115,106,94),true)
    addBox(2.3,0.60,1.02,1.08,0.12,0.48,new Color(194,184,166),true)
    for (x <- Seq(1.35,3.25); z <- Seq(0.85,3.25))
      addBox(x,0.13,z,0.17,0.26,0.17,DARK_WOOD,true)

    // Wardrobe.
    addBox(wardrobeX,1.08,wardrobeZ,0.92,2.16,0.86,new Color(61,41,30),true)
    addBox(1.73,1.08,wardrobeZ,0.08,2.03,0.82,new Color(83,55,39),true)

    // Tables, cupboards and other simple room props.
    addBox(4.75,0.42,2.3,0.82,0.82,0.82,WOOD,true)
    addBox(4.75,0.86,2.3,0.88,0.08,0.88,DARK_WOOD,true)
    addBox(9.0,0.65,5.0,1.65,0.14,1.2,WOOD,true)
    for (x <- Seq(8.35,9.65); z <- Seq(4.55,5.45))
      addBox(x,0.30,z,0.13,0.60,0.13,DARK_WOOD,true)
    addBox(15.0,0.90,2.0,1.0,1.8,0.65,DARK_WOOD,true)
    addBox(18.6,0.90,8.6,1.05,1.8,0.65,WOOD,true)
    addBox(5.15,0.72,16.45,0.62,1.44,0.58,DARK_WOOD,true)
    addBox(18.5,0.38,14.7,1.3,0.72,0.8,DARK_WOOD,true)

    // Discoverable shelf in the back hall. Interact with it to reveal the panel.
    addBox(clueShelfX,0.95,clueShelfZ,1.05,1.9,0.36,DARK_WOOD,true)
    addBox(clueShelfX,0.42,clueShelfZ,1.18,0.10,0.48,WOOD,false)
    addBox(clueShelfX,1.00,clueShelfZ,1.18,0.10,0.48,WOOD,false)
    addBox(clueShelfX,1.58,clueShelfZ,1.18,0.10,0.48,WOOD,false)
    addBox(clueShelfX,2.08,clueShelfZ,1.18,0.10,0.48,WOOD,false)

    // Secret storage room shelf, behind the panel.
    addBox(secretShelfX,1.02,secretShelfZ,1.10,2.04,0.40,DARK_WOOD,true)
    addBox(secretShelfX,0.42,secretShelfZ,1.22,0.10,0.48,WOOD,false)
    addBox(secretShelfX,1.00,secretShelfZ,1.22,0.10,0.48,WOOD,false)
    addBox(secretShelfX,1.58,secretShelfZ,1.22,0.10,0.48,WOOD,false)
    addBox(secretShelfX,2.08,secretShelfZ,1.22,0.10,0.48,WOOD,false)

    // Bathroom fixtures in their own room: bathtub, toilet, sink and mirror.
    addBox(21.85,0.30,4.70,2.35,0.55,1.05,new Color(147,145,132),true)
    addBox(21.85,0.58,4.70,2.02,0.12,0.78,new Color(185,183,171),false)
    addBox(20.05,0.27,2.45,0.72,0.42,0.82,new Color(185,181,168),true)
    addBox(20.05,0.56,2.58,0.55,0.22,0.48,new Color(205,202,189),false)
    addBox(22.85,0.48,1.55,0.80,0.95,0.60,new Color(105,103,96),true)
    addBox(22.85,1.02,1.55,0.90,0.12,0.68,new Color(186,183,171),false)
    addBox(22.15,1.95,0.28,1.00,0.82,0.08,new Color(42,57,61),false)

    // Decorative climbable ladder connecting the lower hall to the upper wing.
    addBox(ladderX-0.24,1.50,ladderZ,0.07,2.95,0.08,DARK_WOOD,false)
    addBox(ladderX+0.24,1.50,ladderZ,0.07,2.95,0.08,DARK_WOOD,false)
    for (i <- 0 until 10) {
      addBox(ladderX,0.18+i*0.29,ladderZ,0.52,0.065,0.08,WOOD,false)
    }

    // Stairs: 10 actual polygonal steps; the collision floor follows the ramp.
    for (i <- 0 until 10) {
      val h = (i+1)*0.30
      val zc = 6.5 + (i+0.5)*0.40
      addBox(15.8,h/2,zc,3.0,h,0.42,new Color(87,63,46),false)
    }
    // Solid side walls keep the player on the stairs.
    addBox(14.20,1.45,8.5,0.18,2.9,4.2,DARK_WOOD,true)
    addBox(17.40,1.45,8.5,0.18,2.9,4.2,DARK_WOOD,true)
    // Ground floor wall prevents walking through the underside of the upper wing.
    addWallZ(10.5,17.45,23.75,Seq(),0.0,2.8,WALL_ALT)

    // Upper floor platform/wing. Top is y = 3.0.
    addBox(19.0,2.90,14.05,9.4,0.20,7.10,new Color(71,56,43),false)
    // Upper-level walls.
    addWallX(14.30,10.55,17.62,Seq((13.35,14.65)),3.0,2.8,WALL_ALT)
    addWallX(23.75,10.50,17.65,Seq(),3.0,2.8,WALL_ALT)
    addWallZ(10.50,17.35,23.75,Seq(),3.0,2.8,WALL_ALT)
    addWallZ(17.62,14.30,23.75,Seq(),3.0,2.8,WALL_ALT)
    addWallX(18.50,10.55,17.60,Seq((12.75,14.05)),3.0,2.8,WALL)
    addWallZ(14.00,18.50,23.70,Seq((20.0,21.3)),3.0,2.8,WALL)
    addDoor(14.30,14.0,1.3,"X",1.0,3.0,2.65,false,false,"Upper door")
    addDoor(18.50,13.4,1.3,"X",-1.0,3.0,2.65,false,false,"Upper hall door")
    addDoor(20.65,14.0,1.3,"Z",1.0,3.0,2.65,false,false,"Upper room door")

    // Upper-floor furniture at y=3.
    addBox(20.8,3.35,12.1,1.5,0.7,0.9,WOOD,true)
    addBox(22.1,3.55,15.5,1.0,1.1,0.7,DARK_WOOD,true)

    // Wall windows: dark glass plus pale frames.
    addBox(3.0,2.0,0.24,1.4,0.8,0.08,new Color(28,43,51),false)
    addBox(14.0,2.0,0.24,1.4,0.8,0.08,new Color(28,43,51),false)
    addBox(10.0,2.0,0.24,0.62,0.72,0.08,new Color(52,41,31),false)
  }

  // ------------------------ Floor & collision ------------------------

  def inStair(x: Double, z: Double): Boolean =
    x >= 14.30 && x <= 17.30 && z >= 6.50 && z <= 10.50

  def floorY(x: Double, z: Double, upper: Boolean): Double = {
    if (inStair(x,z)) 3.0 * clamp((z-6.50)/4.0,0.0,1.0)
    else if (upper) 3.0
    else 0.0
  }

  def updateLevel(upper: Boolean, x: Double, z: Double): Boolean = {
    if (inStair(x,z)) {
      if (z >= 10.46) true
      else if (z <= 6.54) false
      else upper
    } else upper
  }

  def boxBlocks(b: Box, x: Double, z: Double, upper: Boolean): Boolean = {
    val feet = floorY(x,z,upper)
    val bodyBottom = feet + 0.05
    val bodyTop = feet + 1.65
    val bBottom = b.y - b.sy/2
    val bTop = b.y + b.sy/2
    val vertical = bodyTop > bBottom && bodyBottom < bTop
    vertical && math.abs(x-b.x) < b.sx/2 + 0.19 &&
      math.abs(z-b.z) < b.sz/2 + 0.19
  }

  def doorBlocks(d: Door, x: Double, z: Double, upper: Boolean): Boolean = {
    val feet = floorY(x,z,upper)
    val overlapsY = feet + 1.65 > d.yBase && feet < d.yBase + d.height
    if (!overlapsY || d.angle > 0.92) false
    else if (d.plane == "X")
      math.abs(x-d.x) < 0.34 && math.abs(z-d.z) < d.width/2 + 0.20
    else
      math.abs(z-d.z) < 0.34 && math.abs(x-d.x) < d.width/2 + 0.20
  }

  def blocked(x: Double, z: Double, upper: Boolean): Boolean = {
    if (x < 0.35 || x > 23.65 || z < 0.35) return true
    if (z > 17.95) {
      val exitPassage = doors.exists(d => d.isExit && d.angle > 1.0 &&
        x > 19.35 && x < 21.65 && z < 19.1)
      if (!exitPassage) return true
    }
    solids.exists(b => boxBlocks(b,x,z,upper)) || doors.exists(d => doorBlocks(d,x,z,upper))
  }

  def movePlayer(dx: Double, dz: Double): Unit = {
    val nx = px + dx
    val levelX = updateLevel(onUpperFloor,nx,pz)
    if (!blocked(nx,pz,levelX)) { px = nx; onUpperFloor = levelX }

    val nz = pz + dz
    val levelZ = updateLevel(onUpperFloor,px,nz)
    if (!blocked(px,nz,levelZ)) { pz = nz; onUpperFloor = levelZ }
  }

  // ----------------------------- Gameplay -----------------------------

  def say(s: String, seconds: Double = 2.8): Unit = {
    message = s
    messageTimer = seconds
  }

  def nearestDoor(): Option[Door] = {
    val feet = floorY(px,pz,onUpperFloor)
    val nearby = doors.filter(d =>
      dist(px,pz,d.x,d.z) < 1.75 &&
      feet + 1.65 > d.yBase && feet < d.yBase + d.height
    )
    if (nearby.isEmpty) None else Some(nearby.minBy(d => dist(px,pz,d.x,d.z)))
  }

  def interact(): Unit = {
    if (!started || gameOver || escaped) return

    if (hidden) {
      hidden = false
      say("YOU LEFT " + (if (hidePlace.isEmpty) "YOUR HIDING PLACE" else hidePlace) + ".")
      hidePlace = ""
      return
    }

    if (dist(px,pz,wardrobeX,wardrobeZ) < 1.25 && !onUpperFloor) {
      hidden = true
      hidePlace = "WARDROBE"
      say("HIDDEN IN THE WARDROBE. PRESS E TO LEAVE.")
      return
    }

    if (dist(px,pz,bedX,bedZ) < 1.65 && !onUpperFloor) {
      hidden = true
      hidePlace = "UNDER THE BED"
      say("HIDING UNDER THE BED. PRESS E TO CRAWL OUT.")
      return
    }

    if (dist(px,pz,ladderX,ladderZ) < 1.20) {
      onUpperFloor = !onUpperFloor
      say(if (onUpperFloor) "YOU CLIMBED THE LADDER UPSTAIRS." else "YOU CLIMBED THE LADDER DOWNSTAIRS.",3.0)
      return
    }

    if (dist(px,pz,clueShelfX,clueShelfZ) < 1.45 && !secretUnlocked && !onUpperFloor) {
      secretUnlocked = true
      doors.find(_.name == "Hidden shelf panel").foreach(_.target = 1.35)
      say("SECRET PANEL FOUND! A HIDDEN ROOM IS OPEN.",4.0)
      try { Toolkit.getDefaultToolkit.beep() } catch { case _: Throwable => () }
      return
    }

    if (secretUnlocked && dist(px,pz,secretShelfX,secretShelfZ) < 1.45 && !onUpperFloor) {
      if (!secretLootTaken) {
        secretLootTaken = true
        hasBomb = true
        say("SECRET SHELF: STUN BOMB COLLECTED. PRESS Q TO THROW IT.",4.0)
      } else {
        say("THE SECRET SHELF IS EMPTY.")
      }
      return
    }

    if (dist(px,pz,vaseX,vaseZ) < 1.25 && !vaseFalling && !vaseBroken && !onUpperFloor) {
      vaseFalling = true
      vaseVel = 0.0
      say("THE VASE IS FALLING...",1.5)
      try { Toolkit.getDefaultToolkit.beep() } catch { case _: Throwable => () }
      return
    }

    nearestDoor() match {
      case Some(d) =>
        if (d.name == "Hidden shelf panel" && !secretUnlocked) {
          say("THE PANEL HAS NO HANDLE. SEARCH THE SHELF NEARBY.")
        } else if (d.locked && !hasKey) {
          say("LOCKED. FIND THE KEY FIRST!")
        } else {
          d.target = if (d.target > 0.5) 0.0 else 1.35
          if (d.isExit && d.target > 0.5) say("EXIT DOOR OPENING...")
          else say(if (d.target > 0.5) d.name.toUpperCase + " OPENING" else d.name.toUpperCase + " CLOSING")
        }
      case None => say("NOTHING TO INTERACT WITH HERE.")
    }
  }

  def resetDay(): Unit = {
    day += 1
    if (day > 5) {
      gameOver = true
      say("DAY 5 FINISHED - GAME OVER",20.0)
    } else {
      px = 3.2; pz = 4.15; yaw = math.Pi; pitch = 0.0
      onUpperFloor = false; hidden = false; hidePlace = ""; hasKey = false
      hasBomb = false; bombThrown = false; bombFuse = 0.0; bombBlastTimer = 0.0
      enemyStunnedTimer = 0.0; secretUnlocked = false; secretLootTaken = false
      doors.foreach(d => { d.angle = 0.0; d.target = 0.0 })
      enemyX = 9.4; enemyZ = 8.3; enemyUpper = false
      enemyTargetX = enemyX; enemyTargetZ = enemyZ
      vaseY = 0.9; vaseVel = 0.0; vaseFalling = false; vaseBroken = false
      noiseTimer = 0.0; wakeProgress = 0.0
      say("DAY " + day + " - YOU WOKE UP AGAIN!",4.0)
      started = true
    }
  }

  def resetGame(): Unit = {
    px = 3.2; pz = 4.15; yaw = math.Pi; pitch = 0.0
    onUpperFloor = false; wakeProgress = 0.0
    started = false; gameOver = false; escaped = false; hidden = false; hidePlace = ""
    hasKey = false; hasBomb = false; bombThrown = false; bombFuse = 0.0; bombBlastTimer = 0.0
    enemyStunnedTimer = 0.0; secretUnlocked = false; secretLootTaken = false
    flashlight = true; showMap = false; day = 1
    doors.foreach(d => { d.angle = 0.0; d.target = 0.0 })
    vaseY = 0.9; vaseVel = 0.0; vaseFalling = false; vaseBroken = false
    noiseTimer = 0.0; enemyX = 9.4; enemyZ = 8.3; enemyUpper = false
    enemyTargetX = enemyX; enemyTargetZ = enemyZ; aiTimer = 0.0; patrolIndex = 0
    message = "DAY 1 - PRESS ENTER TO WAKE UP"; messageTimer = 5.0
  }

  def wake(): Unit = {
    started = true
    wakeProgress = 0.0
    say("DAY 1. GET OUT OF THE BEDROOM.",4.0)
  }

  def playerEyeY: Double = floorY(px,pz,onUpperFloor) + 0.55 + 1.05*wakeProgress

  def updateDoors(dt: Double): Unit = {
    doors.foreach { d =>
      val speed = 1.9*dt
      if (d.angle < d.target) d.angle = math.min(d.target,d.angle+speed)
      else if (d.angle > d.target) d.angle = math.max(d.target,d.angle-speed)
    }
  }

  def walkableGrid(x: Double, z: Double, upper: Boolean): Boolean = !blocked(x,z,upper)

  def gridPathStep(sx: Double, sz: Double, tx: Double, tz: Double,
                   upper: Boolean): (Double,Double) = {
    val cell = 0.8
    val w = 30; val h = 23
    def gx(x: Double) = clamp((x/cell).toInt,0,w-1).toInt
    def gz(z: Double) = clamp((z/cell).toInt,0,h-1).toInt
    val start = gz(sz)*w + gx(sx)
    val goal = gz(tz)*w + gx(tx)
    if (start == goal) return (tx,tz)
    val prev = Array.fill(w*h)(-1)
    val q = mutable.Queue[Int]()
    prev(start) = start; q.enqueue(start)
    val ds = Array((1,0),(-1,0),(0,1),(0,-1))
    var checks = 0
    while (q.nonEmpty && prev(goal) == -1 && checks < w*h) {
      val cur = q.dequeue()
      val cx = cur%w; val cz = cur/w
      for ((dx,dz) <- ds) {
        val nx = cx+dx; val nz = cz+dz
        if (nx>=0 && nz>=0 && nx<w && nz<h) {
          val ni = nz*w+nx
          if (prev(ni) == -1 && walkableGrid(nx*cell+cell/2,nz*cell+cell/2,upper)) {
            prev(ni)=cur; q.enqueue(ni)
          }
        }
      }
      checks += 1
    }
    if (prev(goal) == -1) return (tx,tz)
    var cur = goal
    while (prev(cur) != start && cur != start && prev(cur) >= 0) cur = prev(cur)
    ((cur%w)*cell+cell/2,(cur/w)*cell+cell/2)
  }

  def throwStunBomb(): Unit = {
    if (!started || gameOver || escaped || hidden) return
    if (!hasBomb) {
      say("NO STUN BOMB. SEARCH THE SECRET SHELF.")
      return
    }
    if (bombThrown) {
      say("A STUN BOMB IS ALREADY IN THE AIR.")
      return
    }
    hasBomb = false
    bombThrown = true
    bombX = px + math.sin(yaw) * 0.55
    bombZ = pz + math.cos(yaw) * 0.55
    bombY = playerEyeY - 0.25
    bombDX = math.sin(yaw)
    bombDZ = math.cos(yaw)
    bombFuse = 0.85
    say("STUN BOMB THROWN! KEEP YOUR DISTANCE.",2.0)
  }

  def detonateStunBomb(): Unit = {
    bombThrown = false
    bombFuse = 0.0
    bombBlastTimer = 1.25
    noiseX = bombX
    noiseZ = bombZ
    noiseTimer = 7.0

    val hit = enemyUpper == onUpperFloor && dist(bombX,bombZ,enemyX,enemyZ) < 3.2
    if (hit) {
      enemyStunnedTimer = 7.0
      say("DIRECT HIT! THE PURSUER IS STUNNED.",3.0)
    } else {
      say("STUN BOMB BURST. THE SOUND DREW THE PURSUER.",3.0)
    }
    try { Toolkit.getDefaultToolkit.beep() } catch { case _: Throwable => () }
  }

  def updateEnemy(dt: Double): Unit = {
    if (hidden || gameOver || escaped || enemyStunnedTimer > 0.0) return
    val dPlayer = dist(px,pz,enemyX,enemyZ)
    var tx = enemyX; var tz = enemyZ
    var targetUpper = enemyUpper

    if (noiseTimer > 0.0) {
      // A ground-floor noise can draw the enemy down from the gallery.
      if (enemyUpper && !inStair(enemyX,enemyZ)) { tx = 15.8; tz = 10.30; targetUpper = true }
      else if (enemyUpper && inStair(enemyX,enemyZ)) { tx = 15.8; tz = 6.25; targetUpper = false }
      else { tx = noiseX; tz = noiseZ; targetUpper = false }
    } else if (dPlayer < 10.5) {
      // The pursuer uses the stair opening to travel between the two levels.
      if (onUpperFloor && !enemyUpper) {
        if (enemyZ < 6.45 || enemyX < 14.3 || enemyX > 17.3) { tx = 15.8; tz = 6.70 }
        else { tx = 15.8; tz = 10.75 }
        targetUpper = false
      } else if (!onUpperFloor && enemyUpper) {
        if (enemyZ > 10.55 || enemyX < 14.3 || enemyX > 17.3) { tx = 15.8; tz = 10.30 }
        else { tx = 15.8; tz = 6.25 }
        targetUpper = true
      } else {
        tx = px; tz = pz; targetUpper = onUpperFloor
      }
    } else {
      val p = patrol(patrolIndex)
      tx = p._1; tz = p._2; targetUpper = false
      if (dist(enemyX,enemyZ,tx,tz)<0.8) patrolIndex = (patrolIndex+1)%patrol.length
    }

    // The pursuer can push open ordinary doors when it reaches them.
    doors.foreach { d =>
      if (!d.locked && !d.isExit &&
          (d.name != "Hidden shelf panel" || secretUnlocked) &&
          dist(enemyX,enemyZ,d.x,d.z) < 1.10) {
        d.target = 1.35
      }
    }

    aiTimer += dt
    if (aiTimer >= 0.55) {
      aiTimer = 0.0
      val step = gridPathStep(enemyX,enemyZ,tx,tz,enemyUpper)
      enemyTargetX = step._1; enemyTargetZ = step._2
    }

    val dx = enemyTargetX-enemyX; val dz = enemyTargetZ-enemyZ
    val len = math.sqrt(dx*dx+dz*dz)
    if (len > 0.06) {
      val step = math.min(len,0.95*dt)
      val nx = enemyX + dx/len*step
      val nz = enemyZ + dz/len*step
      val nxLevel = updateLevel(enemyUpper,nx,enemyZ)
      if (!blocked(nx,enemyZ,nxLevel)) { enemyX=nx; enemyUpper=nxLevel }
      val nzLevel = updateLevel(enemyUpper,enemyX,nz)
      if (!blocked(enemyX,nz,nzLevel)) { enemyZ=nz; enemyUpper=nzLevel }
    }

    if (!hidden && enemyUpper == onUpperFloor &&
        dist(px,pz,enemyX,enemyZ)<0.62) resetDay()
  }

  def updateGame(): Unit = {
    val dt = 0.033
    updateDoors(dt)
    if (!started || gameOver || escaped) return
    if (messageTimer > 0.0) messageTimer -= dt
    if (noiseTimer > 0.0) noiseTimer -= dt
    if (enemyStunnedTimer > 0.0) enemyStunnedTimer = math.max(0.0,enemyStunnedTimer-dt)
    if (bombBlastTimer > 0.0) bombBlastTimer = math.max(0.0,bombBlastTimer-dt)
    wakeProgress = math.min(1.0,wakeProgress+dt*0.65)

    if (bombThrown) {
      val nx = bombX + bombDX * 5.2 * dt
      val nz = bombZ + bombDZ * 5.2 * dt
      if (blocked(nx,nz,onUpperFloor)) {
        bombFuse = math.min(bombFuse,0.01)
      } else {
        bombX = nx
        bombZ = nz
        bombY = floorY(bombX,bombZ,onUpperFloor) + 0.24 + 0.08*math.sin((0.85-bombFuse)*10.0)
      }
      bombFuse -= dt
      if (bombFuse <= 0.0) detonateStunBomb()
    }

    if (vaseFalling && !vaseBroken) {
      vaseVel += 9.8*dt
      vaseY -= vaseVel*dt
      if (vaseY <= 0.12) {
        vaseY=0.12; vaseBroken=true; vaseFalling=false
        noiseX=vaseX; noiseZ=vaseZ; noiseTimer=12.0
        say("CRASH! THE ENEMY HEARD THAT!",4.0)
      }
    }

    if (!hidden) {
      var forward=0.0; var side=0.0
      if (keys.contains(KeyEvent.VK_W)) forward += 1.0
      if (keys.contains(KeyEvent.VK_S)) forward -= 1.0
      if (keys.contains(KeyEvent.VK_D)) side += 1.0
      if (keys.contains(KeyEvent.VK_A)) side -= 1.0
      val len=math.sqrt(forward*forward+side*side)
      if (len>0.0) {
        val speed=if (keys.contains(KeyEvent.VK_SHIFT)) 3.3 else 2.0
        val f=forward/len*speed*dt; val s=side/len*speed*dt
        val dx=math.sin(yaw)*f+math.cos(yaw)*s
        val dz=math.cos(yaw)*f-math.sin(yaw)*s
        movePlayer(dx,dz)
      }
    }

    // Bumping the fragile vase also knocks it down; it need not be pressed with E.
    if (!vaseBroken && !vaseFalling && !onUpperFloor && dist(px,pz,vaseX,vaseZ) < 0.68) {
      vaseFalling = true
      vaseVel = 0.0
      say("YOU KNOCKED THE VASE OVER!",1.4)
    }

    if (!hasKey && !onUpperFloor && dist(px,pz,keyX,keyZ)<0.55) {
      hasKey=true; say("KEY COLLECTED. FIND THE EXIT!",3.5)
    }

    val exit = doors.find(_.isExit)
    if (exit.isDefined && exit.get.angle>1.0 && !onUpperFloor &&
        px>19.4 && px<21.6 && pz>18.0) {
      escaped=true; say("YOU ESCAPED THE OLD HOUSE!",20.0)
    }

    updateEnemy(dt)
  }

  // ---------------------- Camera & depth renderer ----------------------

  def camera(v: V3): CV = {
    val dx=v.x-px; val dy=v.y-playerEyeY; val dz=v.z-pz
    val cx=dx*math.cos(yaw)-dz*math.sin(yaw)
    val forward=dx*math.sin(yaw)+dz*math.cos(yaw)
    val cp=math.cos(pitch); val sp=math.sin(pitch)
    CV(cx,dy*cp-forward*sp,dy*sp+forward*cp)
  }

  def clipNear(poly0: Vector[CV]): Vector[CV] = {
    val out=ArrayBuffer[CV]()
    var i=0
    while (i<poly0.length) {
      val a=poly0(i); val b=poly0((i+1)%poly0.length)
      val ia=a.z>=NEAR; val ib=b.z>=NEAR
      if (ia && ib) out += b
      else if (ia && !ib) {
        val t=(NEAR-a.z)/(b.z-a.z)
        out += CV(a.x+(b.x-a.x)*t,a.y+(b.y-a.y)*t,NEAR)
      } else if (!ia && ib) {
        val t=(NEAR-a.z)/(b.z-a.z)
        out += CV(a.x+(b.x-a.x)*t,a.y+(b.y-a.y)*t,NEAR)
        out += b
      }
      i+=1
    }
    out.toVector
  }

  def edge(ax: Double, ay: Double, bx: Double, by: Double,
           px0: Double, py0: Double): Double =
    (px0-ax)*(by-ay)-(py0-ay)*(bx-ax)

  def rasterTriangle(a: CV,b: CV,c: CV,color: Color,w: Int,h: Int): Unit = {
    val ax=w/2.0+a.x*FOCAL/a.z; val ay=h/2.0-a.y*FOCAL/a.z
    val bx=w/2.0+b.x*FOCAL/b.z; val by=h/2.0-b.y*FOCAL/b.z
    val cx=w/2.0+c.x*FOCAL/c.z; val cy=h/2.0-c.y*FOCAL/c.z
    val area=edge(ax,ay,bx,by,cx,cy)
    if (math.abs(area)<0.00001) return

    val minX=math.max(0,math.floor(math.min(ax,math.min(bx,cx))).toInt)
    val maxX=math.min(w-1,math.ceil(math.max(ax,math.max(bx,cx))).toInt)
    val minY=math.max(0,math.floor(math.min(ay,math.min(by,cy))).toInt)
    val maxY=math.min(h-1,math.ceil(math.max(ay,math.max(by,cy))).toInt)
    if (minX>maxX || minY>maxY) return

    val rgb=color.getRGB
    val invA=1.0/a.z; val invB=1.0/b.z; val invC=1.0/c.z
    var y=minY
    while (y<=maxY) {
      var x=minX
      val py0=y+0.5
      while (x<=maxX) {
        val px0=x+0.5
        val wa=edge(bx,by,cx,cy,px0,py0)/area
        val wb=edge(cx,cy,ax,ay,px0,py0)/area
        val wc=1.0-wa-wb
        if (wa >= -0.00001 && wb >= -0.00001 && wc >= -0.00001) {
          val q=wa*invA+wb*invB+wc*invC
          val idx=y*w+x
          if (q>zBuffer(idx)) { zBuffer(idx)=q; pixels(idx)=rgb }
        }
        x+=1
      }
      y+=1
    }
  }

  def dynamicGeometry(): ArrayBuffer[Tri] = {
    val out=ArrayBuffer[Tri]()
    doors.foreach(d => out ++= makeDoorTriangles(d))

    // Key is a small low-poly gold object.
    if (!hasKey) {
      out ++= boxTriangles(Box(keyX,0.20,keyZ,0.42,0.06,0.06,new Color(236,190,42),false))
      out ++= boxTriangles(Box(keyX-0.22,0.20,keyZ,0.13,0.09,0.11,new Color(250,213,66),false))
    }

    // Thrown stun charge and brief blast flash.
    if (bombThrown) {
      out ++= boxTriangles(Box(bombX,bombY,bombZ,0.24,0.24,0.24,new Color(65,190,220),false))
      out ++= boxTriangles(Box(bombX,bombY+0.10,bombZ,0.10,0.10,0.10,new Color(240,220,120),false))
    }
    if (bombBlastTimer > 0.0) {
      val radius = 0.35 + (1.25-bombBlastTimer)*2.2
      out ++= boxTriangles(Box(bombX,bombY,bombZ,radius,0.10,radius,new Color(85,185,220),false))
    }

    // Vase and broken fragments.
    if (!vaseBroken) {
      out ++= boxTriangles(Box(vaseX,vaseY,vaseZ,0.24,0.34,0.24,new Color(118,64,48),false))
      out ++= boxTriangles(Box(vaseX,vaseY+0.17,vaseZ,0.16,0.05,0.16,new Color(155,100,68),false))
    } else {
      out ++= boxTriangles(Box(vaseX-0.12,0.06,vaseZ,0.18,0.05,0.12,new Color(118,64,48),false))
      out ++= boxTriangles(Box(vaseX+0.13,0.05,vaseZ+0.04,0.14,0.04,0.09,new Color(155,100,68),false))
    }

    // Original simple low-poly pursuer (not the Granny character model).
    val ey=if (enemyUpper) 3.0 else floorY(enemyX,enemyZ,false)
    out ++= boxTriangles(Box(enemyX,ey+1.02,enemyZ,0.50,1.12,0.36,new Color(65,17,22),false))
    out ++= boxTriangles(Box(enemyX,ey+1.78,enemyZ,0.42,0.43,0.42,new Color(174,151,127),false))
    out ++= boxTriangles(Box(enemyX,ey+1.99,enemyZ,0.47,0.14,0.47,new Color(28,22,20),false))
    out ++= boxTriangles(Box(enemyX-0.32,ey+1.04,enemyZ,0.17,0.82,0.19,new Color(94,61,47),false))
    out ++= boxTriangles(Box(enemyX+0.32,ey+1.04,enemyZ,0.17,0.82,0.19,new Color(94,61,47),false))
    out ++= boxTriangles(Box(enemyX-0.14,ey+0.31,enemyZ,0.19,0.60,0.22,new Color(35,28,27),false))
    out ++= boxTriangles(Box(enemyX+0.14,ey+0.31,enemyZ,0.19,0.60,0.22,new Color(35,28,27),false))
    out
  }

  def lightColor(t: Tri): Color = {
    val cx=(t.a.x+t.b.x+t.c.x)/3.0
    val cz=(t.a.z+t.b.z+t.c.z)/3.0
    val dx=cx-px; val dz=cz-pz
    val d=math.sqrt(dx*dx+dz*dz)
    val dot=if (d<0.001) 1.0 else (dx*math.sin(yaw)+dz*math.cos(yaw))/d
    val light=if (flashlight) 0.18+0.82*math.max(0.0,dot+0.12)/(1.0+d*0.025)
              else 0.16
    shade(t.color,light)
  }

  def renderWorld(w: Int,h: Int): Unit = {
    if (image==null || image.getWidth!=w || image.getHeight!=h) {
      image=new BufferedImage(w,h,BufferedImage.TYPE_INT_RGB)
      pixels=image.getRaster.getDataBuffer.asInstanceOf[DataBufferInt].getData
      zBuffer=new Array[Double](w*h)
    }
    val bg=image.createGraphics()
    bg.setPaint(new GradientPaint(0f,0f,new Color(9,11,15),0f,(h*0.5).toFloat,new Color(47,40,34)))
    bg.fillRect(0,0,w,h)
    bg.setPaint(new GradientPaint(0f,(h*0.5).toFloat,new Color(47,39,31),0f,h.toFloat,new Color(8,8,10)))
    bg.fillRect(0,h/2,w,h/2)
    bg.dispose()
    java.util.Arrays.fill(zBuffer,0.0)

    val dyn=dynamicGeometry()
    val all=triangles ++ dyn
    all.foreach { t =>
      val clipped=clipNear(Vector(camera(t.a),camera(t.b),camera(t.c)))
      if (clipped.length>=3) {
        val col=lightColor(t)
        var i=1
        while (i<clipped.length-1) {
          rasterTriangle(clipped(0),clipped(i),clipped(i+1),col,w,h)
          i+=1
        }
      }
    }
  }

  def drawMap(g: Graphics2D,w: Int): Unit = {
    val ox=w-176; val oy=100; val scale=6.0
    g.setColor(new Color(0,0,0,180)); g.fillRect(ox-6,oy-6,150,126)
    g.setColor(new Color(125,112,96))
    solids.foreach { b =>
      if (b.y<2.0) g.fillRect(ox+(b.x-b.sx/2).toInt*scale.toInt,
        oy+(b.z-b.sz/2).toInt*scale.toInt,math.max(1,(b.sx*scale).toInt),math.max(1,(b.sz*scale).toInt))
    }
    g.setColor(Color.RED); g.fillOval(ox+(enemyX*scale).toInt-3,oy+(enemyZ*scale).toInt-3,7,7)
    g.setColor(Color.CYAN); g.fillOval(ox+(px*scale).toInt-3,oy+(pz*scale).toInt-3,7,7)
  }

  def drawHUD(g: Graphics2D,w: Int,h: Int): Unit = {
    g.setColor(new Color(0,0,0,175)); g.fillRect(0,0,w,58)
    g.setFont(new Font("Arial",Font.BOLD,16)); g.setColor(new Color(245,222,184))
    g.drawString("DAY "+day+"   KEY: "+(if(hasKey) "YES" else "NO")+
      "   FLOOR: "+(if(onUpperFloor) "UPSTAIRS" else "GROUND")+
      "   SECRET: "+(if(secretUnlocked) "OPEN" else "HIDDEN"),14,23)
    g.setFont(new Font("Arial",Font.PLAIN,13)); g.setColor(Color.WHITE)
    g.drawString("WASD Move | Mouse drag Look | SHIFT Run | E Interact | Q Stun bomb | F Light | M Map | R Restart",14,45)
    g.setColor(new Color(245,222,184))
    g.drawString("STUN BOMB: "+(if(hasBomb) "READY" else "NONE")+
      (if(enemyStunnedTimer>0.0) "   PURSUER STUNNED" else "")+
      (if(hidden) "   HIDING: "+hidePlace else ""),14,76)
    g.setColor(new Color(0,0,0,180)); g.fillRect(0,h-39,w,39)
    if (messageTimer>0.0) {
      g.setFont(new Font("Arial",Font.BOLD,16)); g.setColor(new Color(255,225,170))
      g.drawString(message,14,h-14)
    }
    if (!hidden && enemyUpper==onUpperFloor && dist(px,pz,enemyX,enemyZ)<5.0) {
      g.setFont(new Font("Arial",Font.BOLD,15)); g.setColor(new Color(255,70,60))
      g.drawString("SOMETHING IS NEAR...",w-235,82)
    }
    if (gameOver || escaped) {
      g.setColor(new Color(0,0,0,220)); g.fillRect(0,0,w,h)
      g.setFont(new Font("Arial",Font.BOLD,42)); g.setColor(if(escaped) new Color(90,255,125) else new Color(255,75,60))
      val title=if(escaped) "YOU ESCAPED!" else "GAME OVER"
      g.drawString(title,(w-g.getFontMetrics.stringWidth(title))/2,h/2)
      g.setFont(new Font("Arial",Font.BOLD,20)); g.setColor(Color.WHITE)
      g.drawString("Press R to restart",w/2-95,h/2+46)
    }
  }

  def drawIntro(g: Graphics2D,w: Int,h: Int): Unit = {
    g.setPaint(new GradientPaint(0f,0f,new Color(5,6,9),w.toFloat,h.toFloat,new Color(59,23,20)))
    g.fillRect(0,0,w,h)
    g.setColor(new Color(25,20,18)); g.fillRect(80,85,w-160,h-155)
    g.setColor(new Color(65,45,33)); g.fillRect(130,120,w-260,h-225)
    g.setColor(new Color(32,24,22)); g.fillRect(160,h-170,260,55)
    g.setColor(new Color(100,80,65)); g.fillRect(180,h-195,220,32)
    g.setColor(new Color(40,24,18)); g.fillRect(w-270,150,90,250)
    g.setColor(new Color(210,165,68)); g.fillOval(w-211,275,11,11)
    g.setFont(new Font("Arial",Font.BOLD,45)); g.setColor(new Color(222,211,194))
    val title="OLD HOUSE 3D"; g.drawString(title,(w-g.getFontMetrics.stringWidth(title))/2,70)
    g.setFont(new Font("Arial",Font.BOLD,21)); g.setColor(new Color(255,95,75))
    val d="DAY 1"; g.drawString(d,(w-g.getFontMetrics.stringWidth(d))/2,h-105)
    g.setFont(new Font("Arial",Font.BOLD,20)); g.setColor(new Color(250,220,160))
    val p="PRESS ENTER TO WAKE UP"; g.drawString(p,(w-g.getFontMetrics.stringWidth(p))/2,h-62)
  }

  class GamePanel extends JPanel {
    setPreferredSize(new Dimension(SW,SH)); setFocusable(true); setBackground(Color.BLACK)
    setFocusTraversalKeysEnabled(false)
    override def paintComponent(graphics: Graphics): Unit = {
      super.paintComponent(graphics)
      val g=graphics.create().asInstanceOf[Graphics2D]
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON)
      if (!started) drawIntro(g,getWidth,getHeight)
      else {
        renderWorld(getWidth,getHeight)
        g.drawImage(image,0,0,null)
        g.setColor(new Color(230,230,215,170))
        g.drawLine(getWidth/2-5,getHeight/2,getWidth/2+5,getHeight/2)
        g.drawLine(getWidth/2,getHeight/2-5,getWidth/2,getHeight/2+5)
        drawHUD(g,getWidth,getHeight)
        if (showMap) drawMap(g,getWidth)
      }
      g.dispose()
    }
  }

  def start(): Unit = {
    buildHouse(); resetGame()
    SwingUtilities.invokeLater(new Runnable {
      def run(): Unit = {
        frame=new JFrame("OLD HOUSE 3D - Scala Horror Prototype")
        panel=new GamePanel()
        panel.addKeyListener(new KeyAdapter {
          override def keyPressed(e: KeyEvent): Unit = {
            val k=e.getKeyCode; keys.add(k)
            if (k==KeyEvent.VK_ENTER && !started && !gameOver) wake()
            if (k==KeyEvent.VK_E) interact()
            if (k==KeyEvent.VK_Q) throwStunBomb()
            if (k==KeyEvent.VK_F) { flashlight = !flashlight; say(if(flashlight) "FLASHLIGHT ON" else "FLASHLIGHT OFF") }
            if (k==KeyEvent.VK_M) showMap = !showMap
            if (k==KeyEvent.VK_R) resetGame()
            if (k==KeyEvent.VK_LEFT) yaw -= 0.08
            if (k==KeyEvent.VK_RIGHT) yaw += 0.08
          }
          override def keyReleased(e: KeyEvent): Unit = keys.remove(e.getKeyCode)
        })
        panel.addMouseListener(new MouseAdapter {
          override def mousePressed(e: MouseEvent): Unit = { lastMouseX=e.getX; lastMouseY=e.getY; panel.requestFocusInWindow() }
          override def mouseReleased(e: MouseEvent): Unit = { lastMouseX = -1; lastMouseY = -1 }
        })
        panel.addMouseMotionListener(new MouseMotionAdapter {
          override def mouseDragged(e: MouseEvent): Unit = {
            if (started && !gameOver && !escaped && lastMouseX>=0) {
              val dx=e.getX-lastMouseX; val dy=e.getY-lastMouseY
              yaw += dx*0.007; pitch=clamp(pitch-dy*0.004,-0.55,0.55)
              lastMouseX=e.getX; lastMouseY=e.getY
            }
          }
        })
        panel.addFocusListener(new java.awt.event.FocusAdapter {
          override def focusLost(e: java.awt.event.FocusEvent): Unit = keys.clear()
        })
        frame.setContentPane(panel); frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
        frame.addWindowListener(new java.awt.event.WindowAdapter { override def windowClosed(e: java.awt.event.WindowEvent): Unit = { if (gameTimer != null) gameTimer.stop(); keys.clear() } })
        frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); panel.requestFocusInWindow()
        gameTimer=new Timer(33,new ActionListener { def actionPerformed(e: ActionEvent): Unit = { updateGame(); panel.repaint() } })
        gameTimer.start()
      }
    })
  }
}

import java.awt._
import java.awt.event._
import java.awt.image.BufferedImage
import java.awt.image.DataBufferInt
import java.io.File
import javax.imageio.ImageIO
import javax.swing._
import scala.collection.mutable.ArrayBuffer

// ================================================================
// BABASAHEB 3D STATUE STUDIO
// A self-contained Scala + Swing procedural 3D statue viewer.
// No external libraries or copyrighted model files are required.
// It creates a stylized, colored 3D representation inspired by
// the widely used blue-suit, glasses, pointing-hand, Constitution
// book pose of Dr. B. R. Ambedkar.
// ================================================================

object Babasaheb3DStatue {

  case class V(x: Double, y: Double, z: Double) {
    def +(b: V): V = V(x + b.x, y + b.y, z + b.z)
    def -(b: V): V = V(x - b.x, y - b.y, z - b.z)
    def *(k: Double): V = V(x * k, y * k, z * k)
  }
  case class Tri(a: V, b: V, c: V, color: Color)
  case class TV(x: Double, y: Double, z: Double)
  case class SV(x: Double, y: Double, invZ: Double)

  val triangles = ArrayBuffer[Tri]()

  // Choose a figure from the dropdown. These are original, procedural,
  // low-poly interpretations made only from geometry in this Scala file.
  val figureNames = Array(
    "Dr. Babasaheb Ambedkar",
    "Gautam Buddha",
    "Ganesh Ji",
    "Lord Shiva",
    "Shri Krishna",
    "Lord Rama",
    "Hanuman Ji",
    "Maa Durga",
    "Maa Saraswati",
    "Maa Lakshmi",
    "Lord Vishnu",
    "Mahavir Swami",
    "Sai Baba",
    "Jesus Christ",
    "Guru Nanak Dev Ji"
  )
  var selectedFigure = figureNames(0)

  // Camera / viewer state.
  var yaw = 0.0
  var pitch = 0.02
  var zoom = 9.4
  val targetY = 2.65
  var themeMode = 0
  var showHalo = true
  var showBook = true
  var showStars = true

  val backgroundTop = new Color(8, 14, 35)
  val backgroundBottom = new Color(45, 27, 57)
  val gold = new Color(255, 200, 72)
  val classicBlue = new Color(25, 78, 190)
  val skinClassic = new Color(190, 137, 105)
  val darkClassic = new Color(22, 22, 27)

  def clamp(v: Double, lo: Double, hi: Double): Double =
    math.max(lo, math.min(hi, v))

  def dot(a: V, b: V): Double = a.x * b.x + a.y * b.y + a.z * b.z
  def cross(a: V, b: V): V = V(
    a.y * b.z - a.z * b.y,
    a.z * b.x - a.x * b.z,
    a.x * b.y - a.y * b.x
  )
  def length(v: V): Double = math.sqrt(dot(v, v))
  def norm(v: V): V = {
    val n = length(v)
    if (n < 0.000001) V(0, 1, 0) else V(v.x / n, v.y / n, v.z / n)
  }

  def recolor(role: String, c: Color): Color = {
    if (themeMode == 0) c
    else if (themeMode == 1) { // bronze sculpture
      role match {
        case "glasses" | "eye" | "dark" => new Color(45, 31, 25)
        case "gold" => new Color(222, 167, 69)
        case "base" => new Color(59, 47, 42)
        case "background" => c
        case _ => new Color(151, 91, 53)
      }
    } else if (themeMode == 2) { // golden tribute statue
      role match {
        case "glasses" | "eye" | "dark" => new Color(58, 39, 19)
        case "base" => new Color(22, 39, 83)
        case "background" => c
        case _ => new Color(236, 177, 43)
      }
    } else { // marble
      role match {
        case "glasses" | "eye" | "dark" => new Color(51, 55, 66)
        case "gold" => new Color(170, 139, 76)
        case "base" => new Color(62, 72, 91)
        case "background" => c
        case _ => new Color(220, 222, 224)
      }
    }
  }

  def scaleColor(c: Color, factor: Double): Color = new Color(
    clamp(c.getRed * factor, 0, 255).toInt,
    clamp(c.getGreen * factor, 0, 255).toInt,
    clamp(c.getBlue * factor, 0, 255).toInt
  )

  def addTri(a: V, b: V, c: V, base: Color): Unit = {
    val n = norm(cross(b - a, c - a))
    val light = norm(V(-0.38, 0.82, 0.58))
    val diffuse = math.abs(dot(n, light))
    val factor = 0.47 + 0.53 * diffuse
    triangles += Tri(a, b, c, scaleColor(base, factor))
  }

  def addBox(cx: Double, cy: Double, cz: Double,
             sx: Double, sy: Double, sz: Double, color: Color): Unit = {
    val x0 = cx - sx / 2; val x1 = cx + sx / 2
    val y0 = cy - sy / 2; val y1 = cy + sy / 2
    val z0 = cz - sz / 2; val z1 = cz + sz / 2
    val v = Array(
      V(x0,y0,z0), V(x1,y0,z0), V(x1,y1,z0), V(x0,y1,z0),
      V(x0,y0,z1), V(x1,y0,z1), V(x1,y1,z1), V(x0,y1,z1)
    )
    val f = Array(
      Array(0,1,2), Array(0,2,3), Array(4,7,6), Array(4,6,5),
      Array(0,4,5), Array(0,5,1), Array(3,2,6), Array(3,6,7),
      Array(0,3,7), Array(0,7,4), Array(1,5,6), Array(1,6,2)
    )
    f.foreach(k => addTri(v(k(0)), v(k(1)), v(k(2)), color))
  }

  def addEllipsoid(center: V, rx: Double, ry: Double, rz: Double,
                   color: Color, lat: Int = 14, lon: Int = 22): Unit = {
    def point(i: Int, j: Int): V = {
      val phi = math.Pi * i.toDouble / lat
      val theta = 2.0 * math.Pi * j.toDouble / lon
      V(center.x + rx * math.sin(phi) * math.cos(theta),
        center.y + ry * math.cos(phi),
        center.z + rz * math.sin(phi) * math.sin(theta))
    }
    for (i <- 0 until lat; j <- 0 until lon) {
      val a = point(i, j)
      val b = point(i + 1, j)
      val c = point(i + 1, (j + 1) % lon)
      val d = point(i, (j + 1) % lon)
      addTri(a, b, c, color)
      addTri(a, c, d, color)
    }
  }

  // Cylinder / capsule-like limb between two 3D points.
  def addCylinder(a: V, b: V, rA: Double, rB: Double,
                  color: Color, seg: Int = 12, caps: Boolean = true): Unit = {
    val axis = norm(b - a)
    val helper = if (math.abs(axis.y) < 0.88) V(0,1,0) else V(1,0,0)
    val u = norm(cross(axis, helper))
    val v = norm(cross(axis, u))
    val ringA = (0 until seg).map { i =>
      val t = 2.0 * math.Pi * i / seg
      a + u * (math.cos(t) * rA) + v * (math.sin(t) * rA)
    }.toVector
    val ringB = (0 until seg).map { i =>
      val t = 2.0 * math.Pi * i / seg
      b + u * (math.cos(t) * rB) + v * (math.sin(t) * rB)
    }.toVector
    for (i <- 0 until seg) {
      val j = (i + 1) % seg
      addTri(ringA(i), ringA(j), ringB(j), color)
      addTri(ringA(i), ringB(j), ringB(i), color)
      if (caps) {
        addTri(a, ringA(j), ringA(i), color)
        addTri(b, ringB(i), ringB(j), color)
      }
    }
  }

  def addCircle(cx: Double, cy: Double, z: Double, radius: Double,
                tube: Double, color: Color, seg: Int = 40): Unit = {
    for (i <- 0 until seg) {
      val t0 = 2.0 * math.Pi * i / seg
      val t1 = 2.0 * math.Pi * (i + 1) / seg
      val a = V(cx + radius * math.cos(t0), cy + radius * math.sin(t0), z)
      val b = V(cx + radius * math.cos(t1), cy + radius * math.sin(t1), z)
      addCylinder(a, b, tube, tube, color, 6, false)
    }
  }

  def addTorusXY(cx: Double, cy: Double, cz: Double, major: Double,
                 tube: Double, color: Color, sides: Int = 72,
                 rings: Int = 8): Unit = {
    def p(i: Int, j: Int): V = {
      val t = 2.0 * math.Pi * i / sides
      val u = 2.0 * math.Pi * j / rings
      V(cx + (major + tube * math.cos(u)) * math.cos(t),
        cy + (major + tube * math.cos(u)) * math.sin(t),
        cz + tube * math.sin(u))
    }
    for (i <- 0 until sides; j <- 0 until rings) {
      val a = p(i,j); val b = p((i+1)%sides,j)
      val c = p((i+1)%sides,(j+1)%rings); val d = p(i,(j+1)%rings)
      addTri(a,b,c,color); addTri(a,c,d,color)
    }
  }

  def addGround(): Unit = {
    val c = new Color(28, 34, 54)
    val a = V(-24, -0.035, -22); val b = V(24, -0.035, -22)
    val d = V(-24, -0.035, 22); val e = V(24, -0.035, 22)
    addTri(a,d,e,c); addTri(a,e,b,c)
  }

  def C(role: String, color: Color): Color = recolor(role, color)

  // --------------------------------------------------------------
  // BUILD THE STATUE: face, glasses, pointing arm, book and suit.
  // --------------------------------------------------------------
  def buildScene(): Unit = {
    triangles.clear()
    addGround()

    // Decorative halo stays behind the statue.
    if (showHalo) {
      addTorusXY(0, 2.82, -0.88, 2.23, 0.065, C("gold", gold), 72, 8)
      addTorusXY(0, 2.82, -0.90, 2.05, 0.018, C("gold", new Color(220, 169, 60)), 72, 6)
      for (i <- 0 until 28) {
        val a = i * 2.0 * math.Pi / 28.0
        val p1 = V(2.38 * math.cos(a), 2.82 + 2.38 * math.sin(a), -0.86)
        val p2 = V(2.58 * math.cos(a), 2.82 + 2.58 * math.sin(a), -0.86)
        addCylinder(p1, p2, 0.018, 0.012, C("gold", gold), 6, false)
      }
    }

    // Gentle distant decorative lights.
    if (showStars) {
      val stars = Array(
        (-3.0,4.45),(-2.8,2.1),(-2.45,3.7),(2.8,4.3),(3.0,2.8),
        (2.6,1.25),(-2.5,1.25),(1.9,4.75),(-1.8,4.8),(3.25,3.65)
      )
      stars.foreach { s =>
        addEllipsoid(V(s._1,s._2,-1.15),0.035,0.035,0.025,
          C("gold",new Color(255, 219, 127)),8,10)
      }
    }

    // Pedestal: layered circular 3D plinth.
    addCylinder(V(0,0.02,0), V(0,0.24,0), 1.02, 1.02,
      C("base",new Color(20,46,103)), 48, true)
    addCylinder(V(0,0.24,0), V(0,0.53,0), 0.84, 0.84,
      C("base",new Color(25,65,139)), 48, true)
    addCylinder(V(0,0.53,0), V(0,0.80,0), 0.73, 0.68,
      C("base",new Color(24,48,96)), 48, true)
    addCylinder(V(0,0.79,0), V(0,0.84,0), 0.70, 0.70,
      C("gold",new Color(218,174,72)), 48, true)
    addBox(0,0.41,0.70,1.48,0.36,0.12,C("base",new Color(15,36,83)))
    addBox(0,0.41,0.768,1.34,0.25,0.025,C("gold",new Color(197,151,58)))
    addBox(0,0.41,0.786,1.28,0.20,0.018,C("base",new Color(19,41,89)))

    if (selectedFigure == "Dr. Babasaheb Ambedkar") {
    // Shoes, trousers and polished black shoes.
    addEllipsoid(V(-0.23,0.91,0.10),0.22,0.12,0.34,C("dark",new Color(20,22,27)),12,20)
    addEllipsoid(V(0.23,0.91,0.10),0.22,0.12,0.34,C("dark",new Color(20,22,27)),12,20)
    addBox(-0.23,0.99,0.07,0.26,0.08,0.35,C("dark",new Color(38,39,43)))
    addBox(0.23,0.99,0.07,0.26,0.08,0.35,C("dark",new Color(38,39,43)))

    val pants = C("suit",new Color(24,70,163))
    addCylinder(V(-0.23,0.98,0.00),V(-0.29,2.04,-0.01),0.19,0.23,pants,18,true)
    addCylinder(V(0.23,0.98,0.00),V(0.29,2.04,-0.01),0.19,0.23,pants,18,true)
    addEllipsoid(V(0,2.00,-0.01),0.43,0.30,0.29,pants,14,22)
    addBox(-0.24,1.55,0.00,0.28,0.78,0.34,pants)
    addBox(0.24,1.55,0.00,0.28,0.78,0.34,pants)

    // Shirt and main suit jacket.
    val suit = C("suit",classicBlue)
    val shirt = C("shirt",new Color(241,238,226))
    val skin = C("skin",skinClassic)
    val tie = C("tie",new Color(188,27,47))
    val hair = C("dark",new Color(25,24,27))
    val jacketDark = scaleColor(suit,0.78)

    addEllipsoid(V(0,2.72,-0.015),0.65,0.88,0.36,suit,20,28)
    addEllipsoid(V(-0.48,3.36,-0.015),0.29,0.29,0.31,suit,14,20)
    addEllipsoid(V(0.48,3.36,-0.015),0.29,0.29,0.31,suit,14,20)
    addEllipsoid(V(0,3.46,-0.025),0.38,0.24,0.30,suit,14,20)

    // White shirt panel on the front of the suit.
    addEllipsoid(V(0,2.91,0.285),0.255,0.66,0.105,shirt,18,24)
    addTri(V(-0.24,3.41,0.365),V(-0.04,3.12,0.405),V(-0.13,3.47,0.36),
      C("shirt",new Color(247,245,236)))
    addTri(V(0.24,3.41,0.365),V(0.04,3.12,0.405),V(0.13,3.47,0.36),
      C("shirt",new Color(247,245,236)))

    // Lapels, jacket seams and buttons.
    addTri(V(-0.42,3.43,0.27),V(-0.12,3.02,0.40),V(-0.27,2.72,0.38),jacketDark)
    addTri(V(0.42,3.43,0.27),V(0.12,3.02,0.40),V(0.27,2.72,0.38),jacketDark)
    addTri(V(-0.10,3.42,0.39),V(-0.01,3.10,0.415),V(0.04,3.41,0.39),
      C("shirt",new Color(245,241,230)))
    addTri(V(-0.015,3.10,0.416),V(0.04,3.41,0.39),V(0.13,3.10,0.405),
      C("shirt",new Color(245,241,230)))
    addTri(V(-0.09,3.22,0.43),V(0,3.11,0.445),V(0.09,3.22,0.43),tie)
    addTri(V(-0.085,3.17,0.44),V(0.085,3.17,0.44),V(0.025,2.43,0.44),tie)
    addTri(V(-0.085,3.17,0.44),V(0.025,2.43,0.44),V(-0.025,2.43,0.44),tie)

    for (yy <- Seq(2.55,2.78,3.00)) {
      addEllipsoid(V(0.28,yy,0.345),0.028,0.028,0.022,C("gold",new Color(205,167,72)),8,10)
    }
    addBox(0.39,2.96,0.34,0.20,0.24,0.035,jacketDark)
    addBox(0.39,2.96,0.364,0.15,0.18,0.018,suit)

    // Right arm: extended to point outward (on the viewer's left).
    addCylinder(V(-0.53,3.37,0.00),V(-0.97,3.68,0.08),0.20,0.175,suit,18,true)
    addEllipsoid(V(-0.77,3.55,0.05),0.21,0.22,0.20,suit,14,18)
    addCylinder(V(-0.97,3.68,0.08),V(-1.47,4.01,0.13),0.16,0.125,suit,18,true)
    addCylinder(V(-1.45,4.00,0.13),V(-1.61,4.055,0.15),0.13,0.105,
      C("shirt",new Color(238,234,222)),12,true)
    addEllipsoid(V(-1.72,4.085,0.18),0.19,0.12,0.13,skin,14,18)
    // Pointing index finger and curled remaining fingers.
    addCylinder(V(-1.78,4.13,0.21),V(-2.22,4.32,0.22),0.047,0.027,skin,10,true)
    addEllipsoid(V(-2.235,4.326,0.22),0.038,0.032,0.032,skin,8,10)
    addCylinder(V(-1.82,4.07,0.22),V(-2.04,4.04,0.24),0.039,0.029,skin,10,true)
    addCylinder(V(-1.80,4.04,0.22),V(-1.98,3.985,0.23),0.035,0.026,skin,10,true)
    addCylinder(V(-1.75,4.02,0.22),V(-1.90,3.965,0.22),0.032,0.024,skin,10,true)
    addCylinder(V(-1.67,4.04,0.22),V(-1.76,3.98,0.25),0.034,0.027,skin,10,true)

    // Left arm: bent in toward the Constitution book.
    addCylinder(V(0.51,3.34,0.00),V(0.78,2.96,0.22),0.19,0.16,suit,18,true)
    addEllipsoid(V(0.68,3.13,0.12),0.19,0.22,0.19,suit,14,18)
    addCylinder(V(0.78,2.96,0.22),V(0.59,2.64,0.42),0.15,0.12,suit,18,true)
    addCylinder(V(0.59,2.66,0.42),V(0.54,2.58,0.51),0.12,0.09,
      C("shirt",new Color(240,237,225)),12,true)

    // Constitution book with cover, pages, gold border and page lines.
    if (showBook) {
      addBox(0.43,2.72,0.53,0.48,0.67,0.18,C("book",new Color(100,51,28)))
      addBox(0.43,2.72,0.635,0.425,0.61,0.025,C("book",new Color(115,61,31)))
      addBox(0.43,2.72,0.653,0.38,0.56,0.012,C("gold",new Color(212,174,83)))
      addBox(0.43,2.72,0.661,0.35,0.53,0.01,C("book",new Color(99,47,25)))
      for (yy <- Seq(2.57,2.62,2.67,2.72,2.77,2.82,2.87)) {
        addBox(0.43,yy,0.67,0.25,0.008,0.006,C("gold",new Color(225,194,117)))
      }
      addBox(0.43,3.015,0.663,0.34,0.012,0.008,C("gold",new Color(231,202,132)))
    }

    // Neck, collar, head and ears.
    addCylinder(V(0,3.38,0.00),V(0,3.78,0.00),0.19,0.17,skin,18,true)
    addEllipsoid(V(0,3.70,0.06),0.27,0.14,0.23,shirt,14,20)
    addEllipsoid(V(0,4.22,0.025),0.365,0.49,0.315,skin,24,32)
    addEllipsoid(V(-0.355,4.19,0.015),0.075,0.12,0.07,skin,12,16)
    addEllipsoid(V(0.355,4.19,0.015),0.075,0.12,0.07,skin,12,16)

    // Neatly combed dark hair cap / side sections.
    addEllipsoid(V(0,4.585,-0.005),0.355,0.145,0.30,hair,16,26)
    addEllipsoid(V(-0.30,4.43,0.005),0.09,0.19,0.26,hair,12,18)
    addEllipsoid(V(0.30,4.43,0.005),0.09,0.19,0.26,hair,12,18)
    addEllipsoid(V(0,4.50,0.275),0.26,0.045,0.055,hair,10,18)

    // Eyes, pupils, brows, nose, mouth and iconic round glasses.
    addEllipsoid(V(-0.135,4.265,0.308),0.061,0.043,0.027,
      C("shirt",new Color(247,238,221)),10,14)
    addEllipsoid(V(0.135,4.265,0.308),0.061,0.043,0.027,
      C("shirt",new Color(247,238,221)),10,14)
    addEllipsoid(V(-0.132,4.265,0.337),0.022,0.027,0.014,C("eye",new Color(28,25,24)),8,10)
    addEllipsoid(V(0.138,4.265,0.337),0.022,0.027,0.014,C("eye",new Color(28,25,24)),8,10)
    addCylinder(V(-0.20,4.333,0.32),V(-0.07,4.342,0.326),0.018,0.016,hair,8,true)
    addCylinder(V(0.07,4.342,0.326),V(0.20,4.333,0.32),0.016,0.018,hair,8,true)

    val glasses = C("glasses",new Color(28,29,34))
    addCircle(-0.135,4.265,0.36,0.105,0.014,glasses,32)
    addCircle(0.135,4.265,0.36,0.105,0.014,glasses,32)
    addCylinder(V(-0.03,4.27,0.36),V(0.03,4.27,0.36),0.014,0.014,glasses,8,false)
    addCylinder(V(-0.24,4.28,0.34),V(-0.34,4.24,0.17),0.012,0.009,glasses,8,false)
    addCylinder(V(0.24,4.28,0.34),V(0.34,4.24,0.17),0.012,0.009,glasses,8,false)

    addEllipsoid(V(0,4.13,0.345),0.065,0.105,0.075,skin,12,16)
    addEllipsoid(V(0,4.075,0.395),0.038,0.028,0.027,
      C("skin",new Color(181,121,91)),10,12)
    addCylinder(V(-0.085,3.995,0.316),V(0.085,3.995,0.316),0.013,0.013,
      C("dark",new Color(102,54,47)),8,false)
    addEllipsoid(V(0,3.945,0.302),0.085,0.035,0.035,
      C("skin",new Color(177,117,91)),10,14)

    // Small decorative lapel pin.
    addEllipsoid(V(-0.34,3.18,0.367),0.045,0.045,0.02,
      C("gold",new Color(248,203,76)),10,14)
    } else {
      buildOtherFigure()
    }
  }

  // --------------------------------------------------------------
  // OTHER ORIGINAL PROCEDURAL STATUES
  // These are stylized 3D interpretations, not photo-realistic assets.
  // --------------------------------------------------------------

  def addFace(skin0: Color, hair0: Color, cy: Double,
              hasHair: Boolean = true, closedEyes: Boolean = false,
              foreheadMark: Option[Color] = None): Unit = {
    val skin = C("skin", skin0)
    val hair = C("dark", hair0)
    addCylinder(V(0,cy-0.48,0.00), V(0,cy-0.28,0.00), 0.15,0.14,skin,14,true)
    addEllipsoid(V(0,cy,0.05),0.325,0.425,0.285,skin,20,28)
    addEllipsoid(V(-0.323,cy-0.01,0.02),0.065,0.12,0.07,skin,10,14)
    addEllipsoid(V(0.323,cy-0.01,0.02),0.065,0.12,0.07,skin,10,14)
    if (hasHair) {
      addEllipsoid(V(0,cy+0.30,-0.015),0.325,0.15,0.275,hair,14,24)
      addEllipsoid(V(-0.275,cy+0.19,0.0),0.075,0.18,0.24,hair,10,14)
      addEllipsoid(V(0.275,cy+0.19,0.0),0.075,0.18,0.24,hair,10,14)
    }
    if (closedEyes) {
      addCylinder(V(-0.19,cy+0.015,0.307),V(-0.075,cy+0.005,0.322),0.012,0.012,C("dark",new Color(65,40,32)),8,false)
      addCylinder(V(0.075,cy+0.005,0.322),V(0.19,cy+0.015,0.307),0.012,0.012,C("dark",new Color(65,40,32)),8,false)
    } else {
      addEllipsoid(V(-0.125,cy+0.015,0.315),0.047,0.029,0.018,new Color(247,239,222),8,12)
      addEllipsoid(V(0.125,cy+0.015,0.315),0.047,0.029,0.018,new Color(247,239,222),8,12)
      addEllipsoid(V(-0.125,cy+0.015,0.334),0.018,0.022,0.012,new Color(32,25,22),8,10)
      addEllipsoid(V(0.125,cy+0.015,0.334),0.018,0.022,0.012,new Color(32,25,22),8,10)
      addCylinder(V(-0.19,cy+0.075,0.303),V(-0.07,cy+0.083,0.31),0.013,0.013,hair,8,false)
      addCylinder(V(0.07,cy+0.083,0.31),V(0.19,cy+0.075,0.303),0.013,0.013,hair,8,false)
    }
    addEllipsoid(V(0,cy-0.085,0.335),0.042,0.075,0.04,skin,10,12)
    addCylinder(V(-0.075,cy-0.19,0.305),V(0.075,cy-0.19,0.305),0.012,0.012,C("dark",new Color(110,62,52)),8,false)
    foreheadMark.foreach { mark =>
      addEllipsoid(V(0,cy+0.17,0.333),0.035,0.07,0.02,mark,10,12)
    }
  }

  def addCrown(col0: Color, baseY: Double = 4.34): Unit = {
    val col = C("gold", col0)
    addCylinder(V(0,baseY,0.01),V(0,baseY+0.17,0.01),0.31,0.27,col,20,true)
    addCylinder(V(0,baseY+0.17,0.01),V(0,baseY+0.40,0.01),0.27,0.12,col,18,true)
    addEllipsoid(V(0,baseY+0.42,0.01),0.10,0.08,0.10,col,10,14)
    addEllipsoid(V(0,baseY+0.10,0.285),0.06,0.07,0.035,new Color(45,170,190),8,10)
  }

  def addStandingLegs(legColor0: Color): Unit = {
    val leg = C("robe", legColor0)
    addEllipsoid(V(-0.22,1.15,0.07),0.20,0.25,0.31,leg,12,18)
    addEllipsoid(V(0.22,1.15,0.07),0.20,0.25,0.31,leg,12,18)
    addEllipsoid(V(-0.23,0.92,0.18),0.22,0.10,0.32,C("dark",new Color(62,43,31)),12,18)
    addEllipsoid(V(0.23,0.92,0.18),0.22,0.10,0.32,C("dark",new Color(62,43,31)),12,18)
  }

  def addStandingTorso(skin0: Color, cloth0: Color, sash0: Color): Unit = {
    val skin = C("skin",skin0)
    val cloth = C("robe",cloth0)
    val sash = C("gold",sash0)
    addStandingLegs(cloth)
    addEllipsoid(V(0,1.72,0.00),0.48,0.40,0.34,cloth,16,22)
    addEllipsoid(V(0,2.58,0.00),0.52,0.76,0.34,cloth,18,24)
    addCylinder(V(0,3.13,0.00),V(0,3.48,0.00),0.15,0.14,skin,14,true)
    addCylinder(V(-0.42,2.88,0.00),V(-0.70,2.47,0.16),0.15,0.12,cloth,14,true)
    addCylinder(V(-0.70,2.47,0.16),V(-0.72,2.21,0.34),0.12,0.095,skin,12,true)
    addEllipsoid(V(-0.72,2.18,0.36),0.13,0.12,0.12,skin,10,14)
    addCylinder(V(0.42,2.88,0.00),V(0.70,2.47,0.16),0.15,0.12,cloth,14,true)
    addCylinder(V(0.70,2.47,0.16),V(0.72,2.21,0.34),0.12,0.095,skin,12,true)
    addEllipsoid(V(0.72,2.18,0.36),0.13,0.12,0.12,skin,10,14)
    addTri(V(-0.27,3.0,0.30),V(0.26,2.28,0.37),V(0.05,2.05,0.38),sash)
    addCylinder(V(-0.39,1.98,0.27),V(0.39,1.98,0.27),0.035,0.035,sash,8,false)
  }

  def buildBuddha(): Unit = {
    val skin = new Color(198,145,109)
    val robe = C("robe",new Color(208,116,35))
    val fold = C("gold",new Color(245,183,68))
    // Seated lotus posture and draped robe.
    addEllipsoid(V(0,1.31,0.06),0.76,0.30,0.47,robe,18,26)
    addEllipsoid(V(-0.39,1.26,0.18),0.43,0.22,0.40,robe,14,22)
    addEllipsoid(V(0.39,1.26,0.18),0.43,0.22,0.40,robe,14,22)
    addEllipsoid(V(0,1.82,0.01),0.54,0.46,0.39,robe,18,24)
    addEllipsoid(V(0,2.39,0.00),0.52,0.62,0.35,robe,18,24)
    addEllipsoid(V(-0.43,2.57,0.00),0.25,0.27,0.27,robe,12,18)
    addEllipsoid(V(0.43,2.57,0.00),0.25,0.27,0.27,robe,12,18)
    addCylinder(V(-0.43,2.55,0.02),V(-0.28,2.02,0.35),0.16,0.12,robe,14,true)
    addCylinder(V(0.43,2.55,0.02),V(0.28,2.02,0.35),0.16,0.12,robe,14,true)
    addEllipsoid(V(-0.16,1.96,0.40),0.24,0.10,0.12,skin,12,18)
    addEllipsoid(V(0.16,1.98,0.42),0.24,0.10,0.12,skin,12,18)
    addTri(V(-0.36,2.79,0.28),V(0.14,2.15,0.40),V(0.30,2.78,0.27),fold)
    addCylinder(V(0,2.72,0.00),V(0,3.05,0.00),0.14,0.14,skin,14,true)
    addFace(skin,new Color(65,45,31),3.45,false,true,None)
    addEllipsoid(V(0,3.86,0.04),0.19,0.15,0.18,skin,12,18) // ushnisha
    // Small curled hair dots, intentionally understated.
    for (row <- 0 until 3; col <- -4 to 4 if math.abs(col)+row < 7) {
      addEllipsoid(V(col*0.064,3.67+row*0.062,0.245-row*0.018),0.026,0.026,0.022,
        new Color(48,44,39),6,8)
    }
    addTorusXY(0,1.72,0.25,0.42,0.025,fold,36,6)
  }

  def buildGanesh(): Unit = {
    val skin = new Color(218,151,133)
    val robe = C("robe",new Color(191,85,40))
    val goldC = C("gold",new Color(245,190,55))
    // Seated body and round belly.
    addEllipsoid(V(0,1.25,0.08),0.65,0.28,0.43,C("robe",new Color(226,157,54)),16,22)
    addEllipsoid(V(0,1.90,0.00),0.50,0.44,0.37,robe,16,22)
    addEllipsoid(V(0,2.34,0.00),0.48,0.58,0.34,robe,16,22)
    addCylinder(V(-0.43,2.52,0.00),V(-0.82,2.16,0.25),0.14,0.10,robe,12,true)
    addCylinder(V(-0.82,2.16,0.25),V(-0.73,2.03,0.36),0.10,0.08,skin,10,true)
    addCylinder(V(0.43,2.52,0.00),V(0.82,2.16,0.25),0.14,0.10,robe,12,true)
    addCylinder(V(0.82,2.16,0.25),V(0.73,2.03,0.36),0.10,0.08,skin,10,true)
    // Elephant head, broad ears, trunk and tusks.
    addCylinder(V(0,2.67,0.00),V(0,3.00,0.00),0.15,0.15,skin,12,true)
    addEllipsoid(V(0,3.50,0.03),0.40,0.46,0.34,skin,18,24)
    addEllipsoid(V(-0.48,3.56,-0.01),0.30,0.19,0.10,skin,12,18)
    addEllipsoid(V(0.48,3.56,-0.01),0.30,0.19,0.10,skin,12,18)
    addEllipsoid(V(-0.14,3.59,0.342),0.055,0.035,0.022,new Color(35,29,27),8,10)
    addEllipsoid(V(0.14,3.59,0.342),0.055,0.035,0.022,new Color(35,29,27),8,10)
    addCylinder(V(-0.20,3.39,0.32),V(-0.16,3.26,0.37),0.045,0.03,new Color(248,231,193),8,true)
    addCylinder(V(0.20,3.39,0.32),V(0.16,3.26,0.37),0.045,0.03,new Color(248,231,193),8,true)
    addCylinder(V(0.02,3.49,0.36),V(0.05,3.19,0.48),0.13,0.10,skin,12,true)
    addCylinder(V(0.05,3.19,0.48),V(0.19,3.06,0.45),0.10,0.075,skin,12,true)
    addCrown(goldC,3.88)
    // Four symbolic arms.
    addCylinder(V(-0.39,2.55,0.00),V(-0.86,2.85,0.05),0.12,0.09,robe,12,true)
    addEllipsoid(V(-0.91,2.88,0.08),0.13,0.12,0.11,skin,10,12)
    addCylinder(V(0.39,2.55,0.00),V(0.86,2.85,0.05),0.12,0.09,robe,12,true)
    addEllipsoid(V(0.91,2.88,0.08),0.13,0.12,0.11,skin,10,12)
    addEllipsoid(V(-0.95,2.90,0.08),0.09,0.10,0.08,goldC,10,12)
    addEllipsoid(V(0.95,2.90,0.08),0.09,0.10,0.08,goldC,10,12)
    // Small mouse companion on the pedestal.
    addEllipsoid(V(0.65,0.94,0.40),0.17,0.09,0.13,new Color(105,100,91),10,14)
    addEllipsoid(V(0.76,1.00,0.40),0.07,0.07,0.07,new Color(105,100,91),8,10)
  }

  def buildShiva(): Unit = {
    val blueSkin = new Color(85,153,205)
    addStandingTorso(blueSkin,new Color(224,182,81),new Color(239,203,103))
    addFace(blueSkin,new Color(50,43,37),4.00,true,false,Some(new Color(195,35,40)))
    addEllipsoid(V(0,4.45,-0.02),0.22,0.26,0.21,new Color(51,47,40),12,18)
    addEllipsoid(V(0.16,4.58,0.00),0.095,0.18,0.08,new Color(60,54,45),10,14)
    // Crescent ornament above the matted hair.
    addCylinder(V(-0.15,4.60,0.05),V(-0.06,4.76,0.05),0.025,0.025,new Color(228,229,235),8,false)
    addCylinder(V(-0.06,4.76,0.05),V(0.08,4.78,0.05),0.025,0.025,new Color(228,229,235),8,false)
    // Rudraksha beads and snake coil.
    addTorusXY(0,3.43,0.22,0.19,0.035,new Color(92,62,36),40,6)
    for (i <- 0 until 18) {
      val a = i*2*math.Pi/18.0
      addEllipsoid(V(0.22*math.cos(a),3.43+0.22*math.sin(a),0.245),0.025,0.025,0.022,
        new Color(126,84,49),6,8)
    }
    // Trident.
    val metal = C("gold",new Color(219,194,129))
    addCylinder(V(1.02,0.94,-0.02),V(1.02,4.48,-0.02),0.035,0.028,WOODEN,10,true)
    addCylinder(V(1.02,4.08,-0.02),V(0.88,4.45,-0.02),0.045,0.012,metal,8,true)
    addCylinder(V(1.02,4.08,-0.02),V(1.16,4.45,-0.02),0.045,0.012,metal,8,true)
    addCylinder(V(1.02,4.08,-0.02),V(1.02,4.57,-0.02),0.045,0.015,metal,8,true)
  }

  // Shared colour for sacred wooden props.
  val WOODEN = new Color(105,68,42)

  def buildKrishna(): Unit = {
    val skin = new Color(76,133,199)
    addStandingTorso(skin,new Color(240,190,48),new Color(54,156,96))
    addFace(skin,new Color(28,25,23),4.00,true,false,Some(new Color(220,43,48)))
    addCrown(new Color(224,174,49),4.37)
    // Peacock feather.
    addCylinder(V(0.13,4.68,0.02),V(0.29,4.96,0.00),0.025,0.016,new Color(41,114,66),8,true)
    addEllipsoid(V(0.31,4.98,0.01),0.10,0.15,0.035,new Color(35,144,73),10,16)
    addEllipsoid(V(0.32,4.98,0.045),0.055,0.08,0.02,new Color(40,112,193),8,12)
    addEllipsoid(V(0.32,4.98,0.063),0.026,0.045,0.012,new Color(239,191,48),8,10)
    // Flute across the chest.
    addCylinder(V(-0.67,2.63,0.38),V(0.68,2.68,0.38),0.035,0.035,new Color(214,168,72),12,true)
    for (x <- Seq(-0.34,-0.12,0.12,0.34)) {
      addEllipsoid(V(x,2.64,0.415),0.018,0.018,0.01,new Color(54,46,33),6,8)
    }
  }

  def buildRama(): Unit = {
    val skin = new Color(93,151,212)
    addStandingTorso(skin,new Color(230,175,49),new Color(194,48,42))
    addFace(skin,new Color(43,34,26),4.00,true,false,Some(new Color(190,35,37)))
    addCrown(new Color(224,174,49),4.37)
    // Tall curved bow at the figure's left.
    val wood = new Color(118,70,37)
    val pts = Vector(V(-1.05,1.20,0.04),V(-1.20,1.80,0.04),V(-1.26,2.55,0.04),V(-1.16,3.30,0.04),V(-0.94,4.00,0.04))
    pts.sliding(2).foreach(pair => if(pair.length==2) addCylinder(pair(0),pair(1),0.035,0.028,wood,8,true))
    addCylinder(pts.head,pts.last,0.008,0.008,new Color(224,217,194),6,false)
    addCylinder(V(-0.69,2.78,0.10),V(-0.89,2.98,0.10),0.06,0.035,wood,8,true)
  }

  def buildHanuman(): Unit = {
    val skin = new Color(199,121,67)
    val cloth = new Color(207,82,42)
    addStandingTorso(skin,cloth,new Color(243,185,52))
    addFace(skin,new Color(117,66,36),4.00,true,false,Some(new Color(211,35,35)))
    // Broad muzzle and divine crown.
    addEllipsoid(V(0,3.88,0.34),0.18,0.12,0.11,new Color(220,159,113),12,16)
    addEllipsoid(V(-0.10,3.90,0.43),0.025,0.025,0.018,new Color(36,25,20),8,10)
    addEllipsoid(V(0.10,3.90,0.43),0.025,0.025,0.018,new Color(36,25,20),8,10)
    addCrown(new Color(227,173,48),4.38)
    // Curled tail behind the body.
    val tail = Vector(V(0.36,1.55,-0.16),V(0.74,1.90,-0.27),V(0.80,2.32,-0.27),V(0.64,2.55,-0.25))
    tail.sliding(2).foreach(pair => if(pair.length==2) addCylinder(pair(0),pair(1),0.05,0.04,skin,8,true))
    // Gada (mace).
    addCylinder(V(1.03,1.0,-0.02),V(0.93,3.38,-0.02),0.055,0.035,new Color(155,99,42),10,true)
    addEllipsoid(V(0.92,3.48,-0.02),0.20,0.24,0.18,new Color(211,161,63),12,16)
  }

  def buildDurga(): Unit = {
    val skin = new Color(215,153,123)
    val red = C("robe",new Color(174,28,53))
    addStandingLegs(new Color(186,37,56))
    addEllipsoid(V(0,1.80,0),0.53,0.43,0.37,new Color(196,36,57),16,22)
    addEllipsoid(V(0,2.58,0),0.57,0.78,0.37,red,18,24)
    addTri(V(-0.40,3.20,0.31),V(0.36,2.14,0.39),V(0.10,1.90,0.40),new Color(239,186,65))
    addCylinder(V(0,3.10,0),V(0,3.47,0),0.15,0.14,skin,14,true)
    addFace(skin,new Color(55,35,25),4.00,true,false,Some(new Color(203,27,45)))
    addCrown(new Color(236,182,56),4.38)
    // Six arms, symbolically holding divine tools.
    val armEnds = Seq((-1.05,3.18),(1.05,3.18),(-1.18,2.65),(1.18,2.65),(-0.95,2.14),(0.95,2.14))
    armEnds.zipWithIndex.foreach { case ((ex,ey),i) =>
      val sy = if (i<2) 3.05 else if (i<4) 2.70 else 2.35
      val sx = if (ex<0) -0.43 else 0.43
      addCylinder(V(sx,sy,0.0),V(ex,ey,0.12),0.12,0.08,red,10,true)
      addEllipsoid(V(ex,ey,0.13),0.10,0.11,0.10,skin,10,12)
    }
    // A lion companion sits beside the pedestal.
    addEllipsoid(V(0.83,1.06,0.34),0.26,0.18,0.25,new Color(197,139,49),12,16)
    addEllipsoid(V(1.02,1.18,0.37),0.15,0.14,0.15,new Color(221,167,61),10,14)
    addEllipsoid(V(1.08,1.20,0.48),0.035,0.03,0.022,new Color(25,23,22),8,10)
  }

  def buildSaraswati(): Unit = {
    val skin = new Color(218,169,139)
    val white = C("robe",new Color(236,229,207))
    addStandingTorso(skin,white,new Color(211,173,76))
    addFace(skin,new Color(57,40,30),4.00,true,false,None)
    addCrown(new Color(220,175,66),4.37)
    // Veena diagonally across body, built from simple 3D parts.
    addCylinder(V(-0.90,2.15,0.38),V(0.72,3.10,0.35),0.045,0.035,new Color(143,80,37),12,true)
    addEllipsoid(V(-0.84,2.12,0.39),0.24,0.21,0.15,new Color(174,103,50),12,16)
    addEllipsoid(V(0.69,3.08,0.36),0.12,0.16,0.10,new Color(196,132,63),10,14)
    for (i <- 0 until 5) {
      addCylinder(V(-0.55,2.35+i*0.12,0.425),V(0.48,2.95+i*0.03,0.40),0.008,0.008,new Color(236,214,153),6,false)
    }
    // Swan at the side.
    addEllipsoid(V(0.88,0.98,0.33),0.20,0.11,0.26,new Color(243,239,220),10,14)
    addCylinder(V(0.96,1.02,0.35),V(1.03,1.22,0.35),0.05,0.035,new Color(243,239,220),8,true)
    addEllipsoid(V(1.04,1.23,0.35),0.09,0.08,0.08,new Color(243,239,220),8,10)
    addCylinder(V(1.09,1.22,0.42),V(1.18,1.20,0.42),0.025,0.01,new Color(223,137,45),6,true)
  }

  def buildLakshmi(): Unit = {
    val skin = new Color(219,161,132)
    val pink = C("robe",new Color(176,33,76))
    addStandingTorso(skin,pink,new Color(238,188,57))
    addFace(skin,new Color(55,37,29),4.00,true,false,Some(new Color(207,32,45)))
    addCrown(new Color(238,187,58),4.37)
    // Lotus petals around the base.
    for (i <- 0 until 12) {
      val a = i * 2.0 * math.Pi / 12.0
      addEllipsoid(V(0.67*math.cos(a),0.99,0.67*math.sin(a)),0.17,0.08,0.23,
        new Color(232,109,153),10,14)
    }
    // Gold coins symbolising abundance.
    for (i <- 0 until 5) {
      addEllipsoid(V(0.60+i*0.08,2.04-i*0.12,0.44),0.035,0.035,0.025,new Color(255,209,66),8,10)
    }
    addEllipsoid(V(-0.78,2.25,0.36),0.14,0.18,0.12,new Color(237,173,56),10,14)
  }

  def buildVishnu(): Unit = {
    val skin = new Color(82,139,206)
    addStandingTorso(skin,new Color(236,189,54),new Color(52,126,77))
    addFace(skin,new Color(48,35,26),4.00,true,false,Some(new Color(210,40,45)))
    addCrown(new Color(228,177,51),4.37)
    // Four arms and the conch/disc symbols.
    addCylinder(V(-0.42,2.94,0),V(-0.90,3.26,0.02),0.12,0.08,new Color(235,184,50),10,true)
    addEllipsoid(V(-0.94,3.29,0.04),0.13,0.13,0.11,new Color(246,226,184),10,12)
    addCylinder(V(0.42,2.94,0),V(0.90,3.26,0.02),0.12,0.08,new Color(235,184,50),10,true)
    addTorusXY(0.94,3.29,0.16,0.12,0.025,new Color(228,182,61),24,6)
    addCylinder(V(-0.42,2.55,0),V(-0.96,2.30,0.02),0.12,0.08,new Color(235,184,50),10,true)
    addEllipsoid(V(-1.01,2.28,0.03),0.14,0.14,0.10,new Color(231,224,204),10,12)
    addCylinder(V(0.42,2.55,0),V(0.96,2.30,0.02),0.12,0.08,new Color(235,184,50),10,true)
    addEllipsoid(V(1.01,2.28,0.04),0.11,0.16,0.09,new Color(224,180,53),10,12)
  }

  def buildMahavir(): Unit = {
    val skin = new Color(206,158,124)
    val robe = C("robe",new Color(226,220,202))
    addEllipsoid(V(0,1.31,0.06),0.73,0.28,0.45,robe,16,22)
    addEllipsoid(V(-0.38,1.28,0.18),0.40,0.20,0.38,robe,12,18)
    addEllipsoid(V(0.38,1.28,0.18),0.40,0.20,0.38,robe,12,18)
    addEllipsoid(V(0,1.91,0.01),0.50,0.44,0.35,robe,16,22)
    addEllipsoid(V(0,2.44,0.00),0.51,0.60,0.34,robe,16,22)
    addCylinder(V(-0.42,2.58,0),V(-0.26,2.03,0.35),0.14,0.11,robe,12,true)
    addCylinder(V(0.42,2.58,0),V(0.26,2.03,0.35),0.14,0.11,robe,12,true)
    addEllipsoid(V(-0.12,1.99,0.39),0.20,0.08,0.10,skin,10,14)
    addEllipsoid(V(0.12,1.99,0.39),0.20,0.08,0.10,skin,10,14)
    addFace(skin,new Color(64,47,35),3.48,false,true,None)
    addEllipsoid(V(0,3.89,0.04),0.18,0.14,0.17,skin,10,14)
  }

  def buildSaiBaba(): Unit = {
    val skin = new Color(196,145,112)
    val robe = C("robe",new Color(231,227,212))
    addEllipsoid(V(0,1.48,0.05),0.55,0.48,0.39,robe,16,20)
    addEllipsoid(V(0,2.08,0.00),0.51,0.62,0.35,robe,16,22)
    addCylinder(V(-0.42,2.42,0),V(-0.30,1.92,0.36),0.14,0.10,robe,12,true)
    addCylinder(V(0.42,2.42,0),V(0.30,1.92,0.36),0.14,0.10,robe,12,true)
    addEllipsoid(V(0,1.88,0.39),0.26,0.10,0.10,skin,12,16)
    addFace(skin,new Color(114,78,53),3.34,false,false,None)
    // Head cloth, moustache and beard.
    addEllipsoid(V(0,3.70,0.00),0.35,0.16,0.31,robe,14,18)
    addEllipsoid(V(0,3.08,0.28),0.20,0.23,0.12,new Color(166,155,141),12,16)
    addCylinder(V(-0.15,3.23,0.30),V(0.0,3.18,0.33),0.03,0.02,new Color(91,66,49),8,true)
    addCylinder(V(0.15,3.23,0.30),V(0.0,3.18,0.33),0.03,0.02,new Color(91,66,49),8,true)
  }

  def buildJesus(): Unit = {
    val skin = new Color(195,146,118)
    addStandingTorso(skin,new Color(228,218,190),new Color(153,35,46))
    addFace(skin,new Color(88,48,31),4.00,true,false,None)
    addEllipsoid(V(0,3.72,0.08),0.12,0.24,0.10,new Color(94,54,35),10,14)
    addEllipsoid(V(-0.15,3.82,0.05),0.08,0.25,0.08,new Color(94,54,35),10,14)
    addEllipsoid(V(0.15,3.82,0.05),0.08,0.25,0.08,new Color(94,54,35),10,14)
    // Simple wooden cross behind the figure.
    addBox(0.93,2.70,-0.42,0.11,2.35,0.10,new Color(104,66,40))
    addBox(0.93,3.25,-0.40,0.95,0.12,0.11,new Color(104,66,40))
  }

  def buildGuruNanak(): Unit = {
    val skin = new Color(197,151,121)
    val robe = C("robe",new Color(224,195,132))
    addEllipsoid(V(0,1.47,0.03),0.58,0.44,0.37,robe,16,20)
    addEllipsoid(V(0,2.12,0),0.53,0.66,0.36,robe,16,22)
    addCylinder(V(-0.42,2.48,0),V(-0.27,1.95,0.35),0.14,0.11,robe,12,true)
    addCylinder(V(0.42,2.48,0),V(0.27,1.95,0.35),0.14,0.11,robe,12,true)
    addEllipsoid(V(0,1.92,0.39),0.25,0.10,0.10,skin,12,16)
    addFace(skin,new Color(91,60,39),3.39,false,false,None)
    addEllipsoid(V(0,3.72,0.00),0.36,0.20,0.32,new Color(233,225,203),14,18) // turban
    addEllipsoid(V(0,3.20,0.29),0.19,0.22,0.12,new Color(158,139,112),12,16) // beard
    addCylinder(V(-0.16,3.26,0.31),V(0,3.20,0.34),0.025,0.02,new Color(90,67,48),8,true)
    addCylinder(V(0.16,3.26,0.31),V(0,3.20,0.34),0.025,0.02,new Color(90,67,48),8,true)
  }

  def buildOtherFigure(): Unit = {
    selectedFigure match {
      case "Gautam Buddha" => buildBuddha()
      case "Ganesh Ji" => buildGanesh()
      case "Lord Shiva" => buildShiva()
      case "Shri Krishna" => buildKrishna()
      case "Lord Rama" => buildRama()
      case "Hanuman Ji" => buildHanuman()
      case "Maa Durga" => buildDurga()
      case "Maa Saraswati" => buildSaraswati()
      case "Maa Lakshmi" => buildLakshmi()
      case "Lord Vishnu" => buildVishnu()
      case "Mahavir Swami" => buildMahavir()
      case "Sai Baba" => buildSaiBaba()
      case "Jesus Christ" => buildJesus()
      case "Guru Nanak Dev Ji" => buildGuruNanak()
      case _ => buildBuddha()
    }
  }

  // --------------------------------------------------------------
  // SOFTWARE 3D RENDERER: near clipping + perspective + Z-buffer.
  // --------------------------------------------------------------
  val nearPlane = 0.18

  def transform(p: V): TV = {
    val cy = math.cos(yaw); val sy = math.sin(yaw)
    val x1 = p.x * cy - p.z * sy
    val z1 = p.x * sy + p.z * cy
    val y1 = p.y - targetY
    val cp = math.cos(pitch); val sp = math.sin(pitch)
    val y2 = y1 * cp - z1 * sp
    val z2 = y1 * sp + z1 * cp
    TV(x1, y2, zoom - z2)
  }

  def clipNear(poly: scala.collection.immutable.List[TV]): scala.collection.immutable.List[TV] = {
    if (poly.isEmpty) return poly
    val out = ArrayBuffer[TV]()
    var previous = poly.last
    var previousInside = previous.z > nearPlane
    poly.foreach { current =>
      val currentInside = current.z > nearPlane
      if (currentInside != previousInside) {
        val den = current.z - previous.z
        if (math.abs(den) > 0.0000001) {
          val t = (nearPlane - previous.z) / den
          out += TV(
            previous.x + (current.x - previous.x) * t,
            previous.y + (current.y - previous.y) * t,
            nearPlane
          )
        }
      }
      if (currentInside) out += current
      previous = current
      previousInside = currentInside
    }
    out.toList
  }

  def project(p: TV, w: Int, h: Int): SV = {
    val focal = math.min(w, h) * 1.03
    SV(w / 2.0 + p.x * focal / p.z,
       h / 2.0 - p.y * focal / p.z,
       1.0 / p.z)
  }

  def rasterTriangle(imgPixels: Array[Int], zbuf: Array[Double],
                     w: Int, h: Int, a: SV, b: SV, c: SV, color: Color): Unit = {
    val area = (b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y)
    if (math.abs(area) < 0.00001) return

    val minX = math.max(0, math.floor(math.min(a.x, math.min(b.x,c.x))).toInt)
    val maxX = math.min(w - 1, math.ceil(math.max(a.x, math.max(b.x,c.x))).toInt)
    val minY = math.max(0, math.floor(math.min(a.y, math.min(b.y,c.y))).toInt)
    val maxY = math.min(h - 1, math.ceil(math.max(a.y, math.max(b.y,c.y))).toInt)
    if (minX > maxX || minY > maxY) return

    val rgb = 0xff000000 | (color.getRed << 16) | (color.getGreen << 8) | color.getBlue
    var y = minY
    while (y <= maxY) {
      val py = y + 0.5
      var x = minX
      while (x <= maxX) {
        val px = x + 0.5
        val wa = ((b.y-c.y)*(px-c.x) + (c.x-b.x)*(py-c.y)) / area
        val wb = ((c.y-a.y)*(px-c.x) + (a.x-c.x)*(py-c.y)) / area
        val wc = 1.0 - wa - wb
        if (wa >= -0.00001 && wb >= -0.00001 && wc >= -0.00001) {
          val inv = wa*a.invZ + wb*b.invZ + wc*c.invZ
          val idx = y*w + x
          if (inv > zbuf(idx)) {
            zbuf(idx) = inv
            imgPixels(idx) = rgb
          }
        }
        x += 1
      }
      y += 1
    }
  }

  def renderImage(w0: Int, h0: Int): BufferedImage = {
    val w = math.max(300,w0); val h = math.max(300,h0)
    val img = new BufferedImage(w,h,BufferedImage.TYPE_INT_ARGB)
    val bg = img.createGraphics()
    bg.setPaint(new GradientPaint(0,0,backgroundTop,0,h,backgroundBottom))
    bg.fillRect(0,0,w,h)
    // A subtle floor horizon glow.
    bg.setColor(new Color(174,113,43,22))
    bg.fillOval(w/2 - 340,h/2 + 100,680,300)
    bg.dispose()

    val pixels = img.getRaster.getDataBuffer.asInstanceOf[DataBufferInt].getData
    val zbuf = Array.fill(w*h)(0.0)

    triangles.foreach { t =>
      val av = transform(t.a); val bv = transform(t.b); val cv = transform(t.c)
      val clipped = clipNear(scala.collection.immutable.List(av,bv,cv))
      if (clipped.length >= 3) {
        val first = project(clipped.head,w,h)
        var i = 1
        while (i < clipped.length - 1) {
          rasterTriangle(pixels,zbuf,w,h,first,
            project(clipped(i),w,h),project(clipped(i+1),w,h),t.color)
          i += 1
        }
      }
    }

    val g = img.createGraphics()
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON)
    drawOverlay(g,w,h)
    g.dispose()
    img
  }

  def projectWorld(x: Double,y: Double,z: Double,w: Int,h: Int): Point = {
    val p = transform(V(x,y,z))
    if (p.z <= nearPlane) new Point(w/2,h/2)
    else {
      val s = project(p,w,h)
      new Point(s.x.toInt,s.y.toInt)
    }
  }

  def drawOverlay(g: Graphics2D,w: Int,h: Int): Unit = {
    // Header and soft dark band.
    g.setColor(new Color(4,8,22,168))
    g.fillRoundRect(16,14,w-32,66,22,22)
    g.setFont(new Font("Serif",Font.BOLD,26))
    g.setColor(new Color(255,218,140))
    val title = selectedFigure.toUpperCase + " 3D STATUE"
    g.drawString(title,(w-g.getFontMetrics.stringWidth(title))/2,43)
    g.setFont(new Font("SansSerif",Font.PLAIN,13))
    g.setColor(new Color(235,238,250))
    val subtitle = "3D STATUE STUDIO  |  DRAG TO ROTATE  |  SCROLL TO ZOOM"
    g.drawString(subtitle,(w-g.getFontMetrics.stringWidth(subtitle))/2,64)

    // Label over the front face of the pedestal when viewed near-front-on.
    if (math.abs(yaw) < 0.55 || math.abs(yaw - 2*math.Pi) < 0.55) {
      val p = projectWorld(0,0.42,0.81,w,h)
      g.setFont(new Font("Serif",Font.BOLD,10))
      g.setColor(new Color(255,221,142))
      val txt = if (selectedFigure == "Dr. Babasaheb Ambedkar") "DR. B. R. AMBEDKAR" else selectedFigure.toUpperCase
      g.drawString(txt,p.x-g.getFontMetrics.stringWidth(txt)/2,p.y+4)
    }

    // Bottom help strip.
    g.setColor(new Color(3,6,17,185))
    g.fillRoundRect(18,h-47,w-36,30,14,14)
    g.setFont(new Font("SansSerif",Font.PLAIN,12))
    g.setColor(new Color(237,240,250))
    val help = "Mouse drag: rotate  |  Wheel / +/-: zoom  |  A/D: rotate  |  Arrows: tilt  |  1-4: finishes  |  H: halo  |  B: book  |  R: reset"
    g.drawString(help,30,h-28)
  }

  // --------------------------------------------------------------
  // UI, themes and screenshot export.
  // --------------------------------------------------------------
  var canvas: JPanel = _
  var statusLabel: JLabel = _
  var frame: JFrame = _

  def resetCamera(): Unit = {
    yaw = 0.0
    pitch = 0.02
    zoom = 9.4
    if (canvas != null) canvas.repaint()
  }

  def saveScreenshot(): Unit = {
    val chooser = new JFileChooser()
    chooser.setDialogTitle("Save statue screenshot as PNG")
    chooser.setSelectedFile(new File(selectedFigure.replaceAll("[^A-Za-z0-9]+", "_") + "_3D_Statue.png"))
    if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
      try {
        val image = renderImage(canvas.getWidth,canvas.getHeight)
        val target = chooser.getSelectedFile
        val file = if (target.getName.toLowerCase.endsWith(".png")) target
                   else new File(target.getParentFile,target.getName + ".png")
        ImageIO.write(image,"png",file)
        statusLabel.setText("Saved: " + file.getAbsolutePath)
      } catch {
        case ex: Exception =>
          JOptionPane.showMessageDialog(frame,"Could not save screenshot: " + ex.getMessage,
            "Save Error",JOptionPane.ERROR_MESSAGE)
      }
    }
  }

  def makeButton(text: String): JButton = {
    val b = new JButton(text)
    b.setFont(new Font("SansSerif",Font.BOLD,13))
    b.setFocusPainted(false)
    b.setBackground(new Color(28,43,81))
    b.setForeground(new Color(245,240,225))
    b.setBorder(BorderFactory.createCompoundBorder(
      BorderFactory.createLineBorder(new Color(207,164,75),1),
      BorderFactory.createEmptyBorder(7,10,7,10)))
    b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
    b
  }

  def start(): Unit = {
    buildScene()
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = {
        frame = new JFrame(selectedFigure + " - 3D Figure Studio - Scala")
        frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
        frame.setMinimumSize(new Dimension(800,650))

        val root = new JPanel(new BorderLayout(0,0))
        root.setBackground(new Color(8,12,28))

        val tools = new JPanel(new GridLayout(2,1,2,2))
        tools.setBackground(new Color(13,20,43))
        val figureBar = new JPanel(new FlowLayout(FlowLayout.CENTER,8,4))
        figureBar.setBackground(new Color(13,20,43))
        val buttonBar = new JPanel(new FlowLayout(FlowLayout.CENTER,8,4))
        buttonBar.setBackground(new Color(13,20,43))

        val figureBox = new JComboBox[String](figureNames)
        figureBox.setSelectedItem(selectedFigure)
        figureBox.setFont(new Font("SansSerif",Font.BOLD,13))
        figureBox.setPreferredSize(new Dimension(245,32))
        figureBox.addActionListener(new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            selectedFigure = figureBox.getSelectedItem.toString
            buildScene()
            resetCamera()
            canvas.repaint()
            frame.setTitle(selectedFigure + " - 3D Figure Studio")
            statusLabel.setText(" Selected: " + selectedFigure + " ? use the mouse to rotate and inspect the model.")
          }
        })

        val themeBox = new JComboBox[String](Array(
          "Classic Colours", "Bronze Sculpture", "Golden Tribute", "White Marble"
        ))
        themeBox.setFont(new Font("SansSerif",Font.BOLD,13))
        themeBox.setPreferredSize(new Dimension(160,32))
        themeBox.addActionListener(new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            themeMode = themeBox.getSelectedIndex
            buildScene()
            canvas.repaint()
          }
        })

        val front = makeButton("FRONT VIEW")
        front.addActionListener(new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = resetCamera()
        })
        val halo = makeButton("TOGGLE HALO (H)")
        halo.addActionListener(new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            showHalo = !showHalo; buildScene(); canvas.repaint()
            statusLabel.setText("Halo: " + (if (showHalo) "ON" else "OFF"))
          }
        })
        val book = makeButton("TOGGLE BOOK (B)")
        book.addActionListener(new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            showBook = !showBook; buildScene(); canvas.repaint()
            statusLabel.setText("Book toggle applies to Dr. Babasaheb Ambedkar's Constitution prop.")
          }
        })
        val shot = makeButton("SAVE PNG")
        shot.addActionListener(new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = saveScreenshot()
        })

        val figureLabel = new JLabel("CLICK TO CHOOSE FIGURE:")
        figureLabel.setForeground(new Color(242,222,173))
        val themeLabel = new JLabel("COLOUR FINISH:")
        themeLabel.setForeground(new Color(242,222,173))
        figureBar.add(figureLabel)
        figureBar.add(figureBox)
        figureBar.add(themeLabel)
        figureBar.add(themeBox)
        buttonBar.add(front); buttonBar.add(halo); buttonBar.add(book); buttonBar.add(shot)
        tools.add(figureBar)
        tools.add(buttonBar)
        root.add(tools,BorderLayout.NORTH)

        canvas = new JPanel() {
          setOpaque(true)
          setBackground(Color.BLACK)
          setFocusable(true)
          override def paintComponent(g: Graphics): Unit = {
            super.paintComponent(g)
            val image = renderImage(getWidth,getHeight)
            g.drawImage(image,0,0,null)
          }
        }
        root.add(canvas,BorderLayout.CENTER)

        statusLabel = new JLabel(" Ready ? choose a figure above, then drag the model to inspect it from every side.")
        statusLabel.setFont(new Font("SansSerif",Font.PLAIN,12))
        statusLabel.setForeground(new Color(230,230,245))
        statusLabel.setBorder(BorderFactory.createEmptyBorder(7,12,7,12))
        statusLabel.setOpaque(true)
        statusLabel.setBackground(new Color(10,15,31))
        root.add(statusLabel,BorderLayout.SOUTH)

        var lastX = 0
        var lastY = 0
        canvas.addMouseListener(new MouseAdapter {
          override def mousePressed(e: MouseEvent): Unit = {
            lastX = e.getX; lastY = e.getY
            canvas.requestFocusInWindow()
          }
        })
        canvas.addMouseMotionListener(new MouseMotionAdapter {
          override def mouseDragged(e: MouseEvent): Unit = {
            val dx = e.getX - lastX
            val dy = e.getY - lastY
            yaw += dx * 0.009
            pitch = clamp(pitch + dy * 0.006,-0.72,0.72)
            lastX = e.getX; lastY = e.getY
            canvas.repaint()
          }
        })
        canvas.addMouseWheelListener(new MouseWheelListener {
          override def mouseWheelMoved(e: MouseWheelEvent): Unit = {
            zoom = clamp(zoom + e.getWheelRotation * 0.45,6.3,15.5)
            canvas.repaint()
          }
        })
        canvas.addKeyListener(new KeyAdapter {
          override def keyPressed(e: KeyEvent): Unit = {
            e.getKeyCode match {
              case KeyEvent.VK_A | KeyEvent.VK_LEFT => yaw -= 0.12
              case KeyEvent.VK_D | KeyEvent.VK_RIGHT => yaw += 0.12
              case KeyEvent.VK_UP => pitch = clamp(pitch - 0.06,-0.72,0.72)
              case KeyEvent.VK_DOWN => pitch = clamp(pitch + 0.06,-0.72,0.72)
              case KeyEvent.VK_ADD | KeyEvent.VK_EQUALS => zoom = clamp(zoom - 0.4,6.3,15.5)
              case KeyEvent.VK_SUBTRACT | KeyEvent.VK_MINUS => zoom = clamp(zoom + 0.4,6.3,15.5)
              case KeyEvent.VK_H => showHalo = !showHalo; buildScene()
              case KeyEvent.VK_B => showBook = !showBook; buildScene()
              case KeyEvent.VK_R => resetCamera()
              case KeyEvent.VK_1 => themeMode = 0; themeBox.setSelectedIndex(0); buildScene()
              case KeyEvent.VK_2 => themeMode = 1; themeBox.setSelectedIndex(1); buildScene()
              case KeyEvent.VK_3 => themeMode = 2; themeBox.setSelectedIndex(2); buildScene()
              case KeyEvent.VK_4 => themeMode = 3; themeBox.setSelectedIndex(3); buildScene()
              case _ => ()
            }
            canvas.repaint()
          }
        })

        frame.setContentPane(root)
        frame.setSize(1220,880)
        frame.setLocationRelativeTo(null)
        frame.setVisible(true)
        canvas.requestFocusInWindow()
      }
    })
  }
}



// ============================================================
// CRICKET LIVE CENTER - integrated app module
// ============================================================
import java.net.{HttpURLConnection, URI, URL}
import java.io.{BufferedReader, InputStreamReader}
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import java.util.Date
import javax.swing.table.DefaultTableModel
import scala.collection.mutable.ArrayBuffer

object YTRCricketLiveCenterApp {
  private val APP_TITLE = "CRICKET LIVE CENTER"
  private val HOME_URL = "https://www.cricbuzz.com/cricket-match/live-scores"
  private val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/153.0 Safari/537.36"
  private val AUTO_REFRESH_MS = 20000

  private val bg = new Color(10,14,24)
  private val panel = new Color(18,24,38)
  private val panel2 = new Color(24,31,48)
  private val white = new Color(245,247,250)
  private val muted = new Color(160,169,184)
  private val green = new Color(45,190,120)
  private val blue = new Color(55,145,255)
  private val orange = new Color(242,159,67)
  private val border = new Color(49,60,82)

  private case class MatchInfo(name: String, url: String, status: String) {
    override def toString: String = status + "  |  " + name
  }


  private def fetch(urlText: String): String = {
    var connection: HttpURLConnection = null
    var reader: BufferedReader = null
    try {
      connection = new URL(urlText).openConnection().asInstanceOf[HttpURLConnection]
      connection.setRequestMethod("GET")
      connection.setRequestProperty("User-Agent", USER_AGENT)
      connection.setRequestProperty("Accept", "text/html,application/xhtml+xml")
      connection.setConnectTimeout(8000)
      connection.setReadTimeout(12000)
      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 out = new StringBuilder
      var line = reader.readLine()
      var total = 0
      while (line != null && total < 1800000) { out.append(line).append("\n"); total += line.length; line = reader.readLine() }
      out.toString
    } finally {
      if (reader != null) try reader.close() catch { case _: Throwable => () }
      if (connection != null) connection.disconnect()
    }
  }

  private def cleanHtml(html: String): String = {
    if (html == null) return ""
    html.replaceAll("(?is)<script[^>]*>.*?</script>", " ")
      .replaceAll("(?is)<style[^>]*>.*?</style>", " ")
      .replaceAll("(?is)<[^>]+>", " ")
      .replace("&nbsp;", " ").replace("&amp;", "&").replace("&quot;", String.valueOf('"'))
      .replace("&#39;", "'").replace("&lt;", "<").replace("&gt;", ">")
      .replaceAll("\\s+", " ").trim
  }

  private def parseMatches(html: String): Vector[MatchInfo] = {
    val out = ArrayBuffer[MatchInfo]()
    val pattern = """(?is)<a[^>]+href=["']([^"']*live-cricket-(?:scores|scorecard)[^"']*)["'][^>]*>(.*?)</a>""".r
    pattern.findAllMatchIn(Option(html).getOrElse("")).foreach { m =>
      val href = m.group(1)
      val name = cleanHtml(m.group(2))
      if (name.length >= 6) {
        val absolute = if (href.startsWith("http")) href else "https://www.cricbuzz.com" + (if (href.startsWith("/")) href else "/" + href)
        val low = name.toLowerCase
        val status = if (low.contains("live")) "LIVE" else if (low.contains("won") || low.contains("result")) "RECENT" else if (low.contains("tomorrow") || low.contains("upcoming")) "UPCOMING" else "MATCH"
        if (!out.exists(_.url == absolute)) out += MatchInfo(name.take(90), absolute, status)
      }
    }
    if (out.isEmpty) Vector(MatchInfo("Cricbuzz Live Scores (open source)", HOME_URL, "LIVE")) else out.take(100).toVector
  }

  private def scoreSummary(html: String): (String,String,String,String,String) = {
    val text = cleanHtml(html)
    val scorePattern = """(\d{1,4}/\d{1,2})\s*\((\d{1,3}(?:\.\d+)?)\s*ov\)""".r
    val found = scorePattern.findFirstMatchIn(text)
    val score = found.map(m => m.group(1) + " (" + m.group(2) + " ov)").getOrElse("Score not available in public HTML")
    val crrPat = """(?i)(?:current run rate|crr)\s*[:\-]?\s*(\d+(?:\.\d+)?)""".r
    val reqPat = """(?i)(?:required run rate|req(?:uired)?\.?\s*rr)\s*[:\-]?\s*(\d+(?:\.\d+)?)""".r
    val targetPat = """(?i)target\s*[:\-]?\s*(\d{1,4})""".r
    val venuePat = """(?i)(?:venue|ground|stadium)\s*[:\-]\s*([A-Za-z0-9 ,'.()&-]{5,100})""".r
    (score, crrPat.findFirstMatchIn(text).map(_.group(1)).getOrElse("?"), reqPat.findFirstMatchIn(text).map(_.group(1)).getOrElse("?"), targetPat.findFirstMatchIn(text).map(_.group(1)).getOrElse("?"), venuePat.findFirstMatchIn(text).map(_.group(1).take(100)).getOrElse("?"))
  }

  private def styledLabel(text: String, size: Int, bold: Boolean = false, color: Color = white): JLabel = {
    val label = new JLabel(text)
    label.setForeground(color)
    label.setFont(new Font("Arial", if (bold) Font.BOLD else Font.PLAIN, size))
    label
  }

  private def appButton(text: String): JButton = {
    val b = new JButton(text)
    b.setFocusPainted(false)
    b.setBackground(new Color(37,48,70))
    b.setForeground(white)
    b.setBorder(javax.swing.BorderFactory.createLineBorder(border))
    b
  }

  // One system-wide laptop unlock only. Cricket Live Center opens directly
  // without its old separate password/login window.
  def start(): Unit = SwingUtilities.invokeLater(new Runnable {
    override def run(): Unit = {
      val window = new CricketWindow()
      window.setVisible(true)
    }
  })

  private class CricketWindow extends JFrame(APP_TITLE) {
    private val matchModel = new DefaultListModel[MatchInfo]()
    private val matchList = new JList[MatchInfo](matchModel)
    private val status = styledLabel("Loading Cricbuzz matches...",11,false,muted)
    private val matchTitle = styledLabel("LIVE CRICKET",20,true,white)
    private val score = styledLabel("?",42,true,white)
    private val crr = styledLabel("CRR  ?",14,true,white)
    private val req = styledLabel("REQ  ?",14,true,white)
    private val target = styledLabel("TARGET  ?",14,true,white)
    private val venue = styledLabel("Venue: ?",12,false,muted)
    private val updated = styledLabel("Updated: ?",11,false,muted)
    private val commentary = new JTextArea()
    commentary.setEditable(false); commentary.setLineWrap(true); commentary.setWrapStyleWord(true)
    commentary.setBackground(panel2); commentary.setForeground(white); commentary.setFont(new Font("Monospaced",Font.PLAIN,12))
    private val tableModel = new DefaultTableModel(Array[Object]("BATTER / PLAYER", "RUNS", "DETAIL"),0) { override def isCellEditable(r:Int,c:Int):Boolean=false }
    private val table = new JTable(tableModel)
    private val tabs = new JTabbedPane()
    private var selected: MatchInfo = MatchInfo("Cricbuzz Live Scores",HOME_URL,"LIVE")
    private val refresh = appButton("REFRESH NOW")
    private val source = appButton("OPEN SOURCE")
    private val timer = new javax.swing.Timer(AUTO_REFRESH_MS,new ActionListener { override def actionPerformed(e:ActionEvent):Unit = refreshSelected() })

    setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
    setSize(1260,820); setMinimumSize(new Dimension(980,650)); setLocationRelativeTo(null)

    private def buildUI(): Unit = {
      val root = new JPanel(new BorderLayout(10,10)); root.setBackground(bg); root.setBorder(javax.swing.BorderFactory.createEmptyBorder(12,12,12,12))
      val top = new JPanel(new BorderLayout()); top.setBackground(panel); top.setBorder(javax.swing.BorderFactory.createEmptyBorder(12,16,12,16))
      val titleStack = new JPanel(new java.awt.GridLayout(2,1)); titleStack.setOpaque(false); titleStack.add(styledLabel("CRICKET LIVE CENTER",22,true,white)); titleStack.add(styledLabel("LIVE SCORES  ?  COMMENTARY  ?  SCORECARD  ?  ANALYTICS",11,false,muted))
      val buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT)); buttons.setOpaque(false); buttons.add(refresh); buttons.add(source); top.add(titleStack,BorderLayout.WEST); top.add(buttons,BorderLayout.EAST)
      val side = new JPanel(new BorderLayout(5,5)); side.setBackground(panel); side.setPreferredSize(new Dimension(330,100)); side.add(styledLabel("MATCHES",14,true,white),BorderLayout.NORTH)
      matchList.setBackground(panel2); matchList.setForeground(white); matchList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
      side.add(new JScrollPane(matchList),BorderLayout.CENTER); side.add(status,BorderLayout.SOUTH)
      val overview = new JPanel(new BorderLayout(8,8)); overview.setBackground(bg); overview.setBorder(javax.swing.BorderFactory.createEmptyBorder(12,4,4,4))
      val scoreBox = new JPanel(new BorderLayout(5,5)); scoreBox.setBackground(panel); scoreBox.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20))
      scoreBox.add(matchTitle,BorderLayout.NORTH); scoreBox.add(score,BorderLayout.CENTER)
      val stats = new JPanel(new FlowLayout(FlowLayout.LEFT,18,8)); stats.setBackground(panel); stats.add(crr); stats.add(req); stats.add(target)
      val details = new JPanel(new java.awt.GridLayout(2,1,4,4)); details.setBackground(panel); details.setBorder(javax.swing.BorderFactory.createEmptyBorder(8,18,12,18)); details.add(venue); details.add(updated)
      val south = new JPanel(new BorderLayout()); south.setBackground(panel); south.add(stats,BorderLayout.NORTH); south.add(details,BorderLayout.CENTER); scoreBox.add(south,BorderLayout.SOUTH); overview.add(scoreBox,BorderLayout.NORTH)
      val overviewInfo = new JTextArea("""Select a match from the left and press REFRESH NOW.

Scores are shown only when successfully parsed from Cricbuzz's public webpage.

Use OPEN SOURCE to view the official source if the page does not expose scores to this simple reader.""")
      overviewInfo.setEditable(false); overviewInfo.setLineWrap(true); overviewInfo.setWrapStyleWord(true); overviewInfo.setBackground(panel2); overviewInfo.setForeground(muted); overviewInfo.setBorder(javax.swing.BorderFactory.createEmptyBorder(12,12,12,12)); overview.add(new JScrollPane(overviewInfo),BorderLayout.CENTER)
      tabs.addTab("OVERVIEW",overview)
      table.setRowHeight(28); table.setBackground(panel2); table.setForeground(white); table.setGridColor(border); table.getTableHeader.setBackground(panel); table.getTableHeader.setForeground(muted)
      tabs.addTab("SCORECARD",new JScrollPane(table))
      tabs.addTab("COMMENTARY",new JScrollPane(commentary))
      val analytics = new JPanel(new BorderLayout()); analytics.setBackground(panel2); analytics.add(styledLabel("RUN / SCORE SUMMARY ? refreshed from the selected source",14,true,white),BorderLayout.NORTH); analytics.add(styledLabel("This panel does not invent missing score data.",12,false,muted),BorderLayout.CENTER); tabs.addTab("ANALYTICS",analytics)
      root.add(top,BorderLayout.NORTH); root.add(side,BorderLayout.WEST); root.add(tabs,BorderLayout.CENTER); setContentPane(root)
      refresh.addActionListener(new ActionListener { override def actionPerformed(e:ActionEvent):Unit = { refreshMatchList(); refreshSelected() } })
      source.addActionListener(new ActionListener { override def actionPerformed(e:ActionEvent):Unit = openSource() })
      matchList.addListSelectionListener(new javax.swing.event.ListSelectionListener { override def valueChanged(e:javax.swing.event.ListSelectionEvent):Unit = if (!e.getValueIsAdjusting && matchList.getSelectedValue != null) { selected=matchList.getSelectedValue; matchTitle.setText(selected.name.toUpperCase); refreshSelected() } })
      addWindowListener(new java.awt.event.WindowAdapter { override def windowClosed(e:java.awt.event.WindowEvent):Unit = timer.stop() })
    }

    private def refreshMatchList(): Unit = {
      status.setText("Reading live match list...")
      val worker = new SwingWorker[Vector[MatchInfo],Void] {
        override def doInBackground():Vector[MatchInfo] = parseMatches(fetch(HOME_URL))
        override def done():Unit = try { val matches=get(); val previous=selected.url; matchModel.clear(); matches.foreach(matchModel.addElement); val idx=matches.indexWhere(_.url==previous); val use=if(idx>=0) idx else 0; if(matches.nonEmpty) { matchList.setSelectedIndex(use); selected=matches(use) }; status.setText(matches.length+" matches ? "+new SimpleDateFormat("HH:mm:ss").format(new Date())) } catch { case ex:Throwable => if(matchModel.isEmpty) { matchModel.addElement(selected); matchList.setSelectedIndex(0) }; status.setText("Source unavailable: "+Option(ex.getMessage).getOrElse("network error")) }
      }; worker.execute()
    }

    private def refreshSelected(): Unit = {
      if (selected == null) return
      status.setText("Refreshing selected match...")
      val targetMatch = selected
      val worker = new SwingWorker[String,Void] {
        override def doInBackground():String = fetch(targetMatch.url)
        override def done():Unit = try {
          val html=get(); val summary=scoreSummary(html); score.setText(summary._1); crr.setText("CRR  "+summary._2); req.setText("REQ  "+summary._3); target.setText("TARGET  "+summary._4); venue.setText("Venue: "+summary._5); updated.setText("Updated: "+new SimpleDateFormat("HH:mm:ss").format(new Date()))
          val text=cleanHtml(html); commentary.setText(if(text.nonEmpty) text.take(12000) else "No readable commentary was returned by the source."); commentary.setCaretPosition(0)
          tableModel.setRowCount(0); tableModel.addRow(Array[Object]("Score",summary._1,"Selected public webpage")); tableModel.addRow(Array[Object]("Current Run Rate",summary._2,"CRR")); tableModel.addRow(Array[Object]("Required Run Rate",summary._3,"REQ")); tableModel.addRow(Array[Object]("Target",summary._4,"If published"))
          status.setText("Source read OK ? "+new SimpleDateFormat("HH:mm:ss").format(new Date()))
        } catch { case ex:Throwable => score.setText("Live score unavailable"); commentary.setText("The public webpage could not be read or did not expose structured score data.\n\nOpen the official source using OPEN SOURCE.\n\nDetails: "+Option(ex.getMessage).getOrElse("network error")); status.setText("Refresh failed: "+Option(ex.getMessage).getOrElse("network error")) }
      }; worker.execute()
    }

    private def openSource(): Unit = try { if (Desktop.isDesktopSupported) Desktop.getDesktop.browse(new URI(selected.url)) else JOptionPane.showMessageDialog(this,selected.url,"Source URL",JOptionPane.INFORMATION_MESSAGE) } catch { case _:Throwable => JOptionPane.showMessageDialog(this,selected.url,"Source URL",JOptionPane.INFORMATION_MESSAGE) }

    buildUI()
    timer.start()
    refreshMatchList()
  }
}



// ============================================================
// FINAL START - launch only the YTR simulated laptop
// ============================================================
SwingUtilities.invokeLater(new Runnable {
  override def run(): Unit = {
    launchUltraLaptop()
  }
})