//
// Copyright (c) 2015, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   5 Jun 2015  Andy Frank  Creation
//

using dom
using graphics

**
** SashBox lays out children in a single direction allowing both
** fixed and pertange sizes that can fill the parent container.
**
** See also: [docDomkit]`docDomkit::Layout#sashBox`
**
@Js class SashBox : Box
{
  new make() : super()
  {
    this.style.addClass("domkit-SashBox")
    this.onEvent("mousedown", true) |e| { onMouseDown(e) }
    this.onEvent("mouseup",   true) |e| { onMouseUp(e)   }
    this.onEvent("mousemove", true) |e| { onMouseMove(e) }
  }

  **
  ** Direction to layout child elements:
  **   - 'Dir.right': layout children left to right
  **   - 'Dir.down': layout childrent top to bottom
  **
  Dir dir := Dir.right

  ** Allow user to resize sash positions. See `div`.
  Bool resizable := false

  ** Callback when user resizes a sash pane if `resizable` is 'true'.
  Void onSashResize(|This| f) { this.cbSashResize = f }

  **
  ** Size to apply to each child, width or height based on `dir`.  Fixed
  ** 'px' and percentage sizes are allowed.  Percentage sizes will be
  ** subtracted from total fixed size using CSS 'calc()' method.
  **
  Str[] sizes := [,]
  {
    set
    {
      &sizes = it
      dims = it.map |s| { CssDim(s) }.toImmutable
      applyStyle
    }
  }

  ** Minimum size a child can be resized to if 'resizable' is 'true'.
  ** Only percentage sizes allowed.
  Str minSize := "10%"

  ** Create a new divider element for resizing children. Dividers are
  ** required between children when `resizable` is 'true'.
  static Elem div()
  {
    Box { it.style.addClass("domkit-SashBox-div") }
  }

  protected override Void onAdd(Elem c)    { applyStyle }
  protected override Void onRemove(Elem c) { applyStyle }

  private Void applyStyle()
  {
    fixed := Str:Float[:]  // unit:sum
    dims.each |d|
    {
      if (d.unit == "%") return
      fixed[d.unit] = (fixed[d.unit] ?: 0f) + d.val.toFloat
    }

    kids := children
    kids.each |kid,i|
    {
      d := dims.getSafe(i)
      if (d == null) return

      css := d.toStr
      if (d.unit == "%" && fixed.size > 0)
      {
        per := fixed.join(" - ") |sum,unit| { "${d.val.toFloat / 100f * sum}$unit" }
        css = "calc($d.toStr - ${per})"
      }

      kid.style->display = css == "0px"
         ? "none"
         : (kid is FlexBox ? "flex" : "block")

      vert := dir == Dir.down
      kid.style->float  = vert ? "none" : "left"
      kid.style->width  = vert ? "100%" : css
      kid.style->height = vert ? css : "100%"
    }
  }

  private Void onMouseDown(Event e)
  {
    if (!resizable) return
    if (resizeIndex == null) return

    e.stop
    div := children[resizeIndex+1]
    this.active = true

    splitter = Elem { it.style.addClass("domkit-resize-splitter") }
    if (dir == Dir.down)
    {
      this.pivoff = this.relPos(e.pagePos).y - div.pos.y
      splitter.style->top    = "${div.pos.y}px"
      splitter.style->width  = "100%"
      splitter.style->height = "${div.size.h}px"
    }
    else
    {
      this.pivoff = this.relPos(e.pagePos).x - div.pos.x
      splitter.style->left   = "${div.pos.x}px"
      splitter.style->width  = "${div.size.w}px"
      splitter.style->height = "100%"
    }

    doc := Win.cur.doc
    Obj? fmove
    Obj? fup

    fmove = doc.onEvent("mousemove", true) |de| { onMouseMove(de) }
    fup = doc.onEvent("mouseup", true) |de| {
      onMouseUp(de)
      de.stop
      doc.removeEvent("mousemove", true, fmove)
      doc.removeEvent("mouseup",   true, fup)
    }

    this.add(splitter)
  }

  private Void onMouseUp(Event e)
  {
    if (!resizable) return
    if (!active) return

    p := this.relPos(e.pagePos)
    kids := children
    if (dir == Dir.down)
    {
      y := 0
      for (i:=0; i<=resizeIndex; i++) y += kids[i].size.h.toInt
      applyResize(resizeIndex, p.y - y - pivoff)
    }
    else
    {
      x := 0
      for (i:=0; i<=resizeIndex; i++) x += kids[i].size.w.toInt
      applyResize(resizeIndex, p.x - x - pivoff)
    }

    this.active = false
    this.resizeIndex = null
    this.remove(splitter)
  }

  private Void onMouseMove(Event e)
  {
    if (!resizable) return

    p := this.relPos(e.pagePos)
    if (active)
    {
      // drag splitter
      if (dir == Dir.down)
      {
        sy := 0f.max(p.y - pivoff).min(this.size.h-splitter.size.h)
        splitter.style->top = "${sy}px"
        e.stop
      }
      else
      {
        sx := 0f.max(p.x - pivoff).min(this.size.w-splitter.size.w)
        splitter.style->left = "${sx}px"
        e.stop
      }
      return
    }
    else
    {
      // check for roll-over cursor
      div := toDiv(e.target)
      if (div != null)
      {
        this.style->cursor = dir==Dir.down ? "row-resize" : "col-resize"
        this.resizeIndex = 0.max(children.findIndex |c| { c == div } - 1)
      }
      else
      {
        this.style->cursor = "default"
        this.resizeIndex = null
      }
    }
  }

  private Elem? toDiv(Elem? elem)
  {
    while (elem != null)
    {
      if (elem.style.hasClass("domkit-SashBox-div") && elem.parent == this) return elem
      elem = elem.parent
    }
    return null
  }

  private Void applyResize(Int index, Float delta)
  {
    // convert to % if needed
    sizesToPercent

    // get adjacent child nodes
    da  := dims[index]
    db  := dims[index+2]

    // if already at minSize bail here
    min := CssDim(minSize).val.toFloat
    dav := da.val.toFloat
    dbv := db.val.toFloat
    if (dav + dbv <= min + min) return

    // split delta between adjacent children
    working := sizes.dup
    sz := dir == Dir.down ? this.size.h : this.size.w
    dp := delta / sz * 100f
    av := (dav + dp).toLocale("0.00", Locale.en).toFloat
    bv := (dav + dbv - av).toLocale("0.00", Locale.en).toFloat
    if (av < min)
    {
      av = min
      bv = (dav + dbv - av).toLocale("0.00", Locale.en).toFloat
    }
    else if (bv < min)
    {
      bv = min
      av = (dav + dbv - bv).toLocale("0.00", Locale.en).toFloat
    }
    working[index]   = "${av}%"
    working[index+2] = "${bv}%"

    // update
    this.sizes = working
    applyStyle
    cbSashResize?.call(this)
  }

  ** Convert `sizes` to %
  @NoDoc Void sizesToPercent()
  {
    // short-circuit if already converted
    if (dims.all |d| { d.unit == "%" }) return

    sz := dir == Dir.down ? this.size.h : this.size.w
    converted := CssDim[,]
    remainder := 100f

    // convert px -> %
    kids := children
    dims.each |d,i|
    {
      if (d.unit == "%") { converted.add(CssDim.defVal); return }
      ksz  := kids.getSafe(i)?.size ?: Size.defVal
      kval := dir == Dir.down ? ksz.h : ksz.w
      val  := ((kval / sz.toFloat) * 100f).toLocale("0.00", Locale.en).toFloat
      converted.add(CssDim(val, "%"))
      remainder -= val
    }

    // divide up existing % into new %
    dims.each |d,i|
    {
      if (d.unit != "%") return
      val := (d.val.toFloat * remainder / 100f).toLocale("0.00", Locale.en).toFloat
      converted[i] = CssDim(val, "%")
    }

    // trim last child to 100% if needed
    sum := 0f
    converted.each |d| { sum += d.val.toFloat }
    if (sum > 100f) converted[-1] = CssDim(converted.last.val.toFloat-(sum-100f), "%")

    // update
    this.sizes = converted.map |c| { c.toStr }
  }

  private CssDim[] dims := CssDim#.emptyList

  private Bool active := false
  private Int? resizeIndex
  private Float? pivoff
  private Elem? splitter
  private Func? cbSashResize
}