///|
pub fn program(
  environment? : @services.AppEnvironment = @services.AppEnvironment::new(),
) -> @moui.Program[ExcelApp, ExcelMsg] {
  ExcelApp::new().program(environment~)
}

///|
pub fn ExcelApp::program(
  self : ExcelApp,
  environment? : @services.AppEnvironment = @services.AppEnvironment::new(),
) -> @moui.Program[ExcelApp, ExcelMsg] {
  let services = environment.services()
  @moui.Program::new(
    init=() => (self, @moui.Effect::none()),
    update=(app, msg) => app.update_with_services(msg, services),
    view=app => app.view(),
  ).with_commands(commands=app => excel_program_commands(app))
}

///|
pub fn excel_program_commands(
  app : ExcelApp,
) -> Array[@moui.ProgramCommand[ExcelMsg]] {
  let model = app.model
  let commands : Array[@moui.ProgramCommand[ExcelMsg]] = []
  let add = command => commands.push(command)
  let add_dual = (label : String, key : String, message : ExcelMsg) => {
    commands.append(excel_dual_commands(label, key, message))
  }

  add(excel_command("Left", "ArrowLeft", CellNavigate(0, -1)))
  add(excel_command("Right", "ArrowRight", CellNavigate(0, 1)))
  add(
    excel_command(
      "Up",
      "ArrowUp",
      if model.autocomplete_suggestions.is_empty() {
        CellNavigate(-1, 0)
      } else {
        AutocompletePrev
      },
    ),
  )
  add(
    excel_command(
      "Down",
      "ArrowDown",
      if model.autocomplete_suggestions.is_empty() {
        CellNavigate(1, 0)
      } else {
        AutocompleteNext
      },
    ),
  )
  add(excel_command("Page Up", "PageUp", CellNavigate(-page_row_step(), 0)))
  add(excel_command("Page Down", "PageDown", CellNavigate(page_row_step(), 0)))
  for
    item in [
      ("Data Left", "ArrowLeft", 0, -1),
      ("Data Right", "ArrowRight", 0, 1),
      ("Data Up", "ArrowUp", -1, 0),
      ("Data Down", "ArrowDown", 1, 0),
    ] {
    let (label, key, drow, dcol) = item
    add(excel_command(label, key, CellNavigateDataEdge(drow, dcol), meta=true))
    add(
      excel_command(label, key, CellNavigateDataEdge(drow, dcol), control=true),
    )
    add(
      excel_command(
        "Extend " + label,
        key,
        SelectionNavigateDataEdge(drow, dcol),
        meta=true,
        shift=true,
      ),
    )
    add(
      excel_command(
        "Extend " + label,
        key,
        SelectionNavigateDataEdge(drow, dcol),
        control=true,
        shift=true,
      ),
    )
  }
  add(excel_command("Next Cell", "Tab", CellNavigate(0, 1)))
  add(excel_command("Previous Cell", "Tab", CellNavigate(0, -1), shift=true))
  let (row, col) = excel_selection_origin(model)
  let sheet = model.active_sheet()
  add(excel_command("Row Start", "Home", CellNavigateTo(row, 0)))
  add(excel_command("Row End", "End", CellNavigateTo(row, sheet.col_count - 1)))
  add(excel_command("Top Left", "Home", CellNavigateTo(0, 0), meta=true))
  add(excel_command("Top Left", "Home", CellNavigateTo(0, 0), control=true))
  add(
    excel_command(
      "Bottom Right",
      "End",
      CellNavigateTo(sheet.row_count - 1, sheet.col_count - 1),
      meta=true,
    ),
  )
  add(
    excel_command(
      "Bottom Right",
      "End",
      CellNavigateTo(sheet.row_count - 1, sheet.col_count - 1),
      control=true,
    ),
  )
  add(
    excel_command(
      "Extend Row Start",
      "Home",
      SelectionNavigateTo(row, 0),
      shift=true,
    ),
  )
  add(
    excel_command(
      "Extend Row End",
      "End",
      SelectionNavigateTo(row, sheet.col_count - 1),
      shift=true,
    ),
  )
  for use_meta in [true, false] {
    add(
      excel_command(
        "Extend Top Left",
        "Home",
        SelectionNavigateTo(0, 0),
        meta=use_meta,
        control=!use_meta,
        shift=true,
      ),
    )
    add(
      excel_command(
        "Extend Bottom Right",
        "End",
        SelectionNavigateTo(sheet.row_count - 1, sheet.col_count - 1),
        meta=use_meta,
        control=!use_meta,
        shift=true,
      ),
    )
  }
  for
    item in [
      ("Extend Left", "ArrowLeft", 0, -1),
      ("Extend Right", "ArrowRight", 0, 1),
      ("Extend Up", "ArrowUp", -1, 0),
      ("Extend Down", "ArrowDown", 1, 0),
      ("Extend Page Up", "PageUp", -page_row_step(), 0),
      ("Extend Page Down", "PageDown", page_row_step(), 0),
    ] {
    let (label, key, drow, dcol) = item
    add(excel_command(label, key, SelectionNavigate(drow, dcol), shift=true))
  }
  add(excel_command("Commit Down", "Enter", excel_commit_message(model, 1, 0)))
  add(
    excel_command(
      "Commit Up",
      "Enter",
      excel_commit_message(model, -1, 0),
      shift=true,
    ),
  )
  add(excel_command("Edit Cell", "F2", CellEditStarted))
  add(excel_command("Cancel Edit", "Escape", CellEditCancelled))
  add_dual("New Workbook", "n", NewWorkbook)
  add_dual("Open File", "o", FileOpenRequested)
  add_dual("Save File", "s", FileSaveRequested)
  add(excel_command("Load Demo", "d", LoadDemo, meta=true))
  add_dual("Cut", "x", CutCells)
  add_dual("Copy", "c", CopyCells)
  add_dual("Paste", "v", PasteCells)
  add_dual("Select All", "a", SelectAllCells)
  add(excel_command("Select Row", " ", RowSelected(row), shift=true))
  add(excel_command("Select Column", " ", ColumnSelected(col), control=true))
  add_dual("Fill Down", "d", FillDown)
  add_dual("Fill Right", "r", FillRight)
  add(excel_command("Undo", "z", Undo, meta=true, intent=Undo))
  add(excel_command("Undo", "z", Undo, control=true, intent=Undo))
  add(excel_command("Redo", "z", Redo, meta=true, shift=true, intent=Redo))
  add(excel_command("Redo", "y", Redo, control=true, intent=Redo))
  add_dual("Bold", "b", ToggleBold)
  add_dual("Italic", "i", ToggleItalic)
  add_dual("Underline", "u", ToggleUnderline)
  add(excel_command("Delete", "Backspace", DeleteSelectedCells))
  add(excel_command("Delete", "Delete", DeleteSelectedCells))
  commands
}

///|
fn excel_command(
  label : String,
  key : String,
  message : ExcelMsg,
  meta? : Bool = false,
  control? : Bool = false,
  shift? : Bool = false,
  intent? : @views.CommandIntent = Activate,
) -> @moui.ProgramCommand[ExcelMsg] {
  @moui.ProgramCommand::new(
    command=@views.ActionCommand::new(
      intent~,
      label~,
      shortcut=Some(
        @views.KeyboardShortcut::new(
          key~,
          modifiers=@views.KeyModifiers::new(meta~, control~, shift~),
        ),
      ),
    ),
    message~,
  )
}

///|
fn excel_dual_commands(
  label : String,
  key : String,
  message : ExcelMsg,
) -> Array[@moui.ProgramCommand[ExcelMsg]] {
  [
    excel_command(label, key, message, meta=true),
    excel_command(label, key, message, control=true),
  ]
}

///|
fn excel_selection_origin(model : ExcelModel) -> (Int, Int) {
  match model.workbook.selection {
    Some(selection) => (selection.row, selection.col)
    None => (0, 0)
  }
}

///|
fn excel_commit_message(model : ExcelModel, drow : Int, dcol : Int) -> ExcelMsg {
  if model.autocomplete_suggestions.is_empty() {
    CellEditCommittedAndNavigate(drow, dcol)
  } else {
    AutocompleteSelect(model.autocomplete_index)
  }
}

///|
fn page_row_step() -> Int {
  15
}