# blesh/contrib/histdb (C) 2023, Koichi Murase <myoga.murase@gmail.com>

ble/util/assign _ble_histdb_sqlite3 'builtin type -P sqlite3 2>/dev/null'
if [[ ! -x $_ble_histdb_sqlite3 ]]; then
  ble/util/print 'contrib/histdb: "sqlite3" is required for this module.' >&2
  return 1
fi

ble-import util.bgproc

_ble_histdb_version=3

#------------------------------------------------------------------------------
# histdb_remarks

bleopt/declare -v histdb_remarks 'git: \q{contrib/histdb-remarks-git}'

_ble_histdb_remarks_data=()
function ble/prompt/unit:_ble_histdb_remarks/update {
  ble/prompt/unit/add-hash '$bleopt_histdb_remarks'
  ble/prompt/unit:{section}/update _ble_histdb_remarks "$bleopt_histdb_remarks" no-trace
}

## @fn ble/histdb/.get-remarks
##   @var[out] remarks
function ble/histdb/.get-remarks {
  remarks=
  if [[ $bleopt_histdb_remarks ]]; then
    local ret
    ble/prompt/unit#update _ble_histdb_remarks "$bleopt_histdb_remarks"
    ble/prompt/unit:{section}/get _ble_histdb_remarks
    remarks=$ret
  fi
}

function ble/prompt/backslash:contrib/histdb-remarks-git {
  ble-import prompt-git

  function ble/prompt/backslash:contrib/histdb-remarks-git {
    local "${_ble_contrib_prompt_git_vars[@]/%/=}" # WA #D1570 checked
    ble/contrib/prompt-git/initialize || return 0
    ble/contrib/prompt-git/update-head-information
    local state; ble/contrib/prompt-git/get-state
    local dirty_mark; ble/contrib/prompt-git/get-dirty-mark

    local path=${git_base%.git}
    path=${path%/}
    path=${path##*?/}
    [[ $PWD == "$git_base"/?* ]] &&
      path="$path/${PWD#$git_base/}"

    ble/prompt/print "git: ${branch:-(detached)} ${hash:-(orphan)}$dirty_mark${state:+ $state} @ ${path:-/}"
  } && "$FUNCNAME"
}

#------------------------------------------------------------------------------
# background sqlite3 process

bleopt/declare -v histdb_ignore ''
bleopt/declare -v histdb_file ''

function ble/histdb/.get-filename {
  local basename=${bleopt_histdb_file%.sqlite3}
  local hostname=${HOSTNAME:-$_ble_base_env_HOSTNAME}
  hostname=${hostname//'/'/'%'} # filename cannot contian /
  ret=${basename:-$_ble_base_state/history}${hostname:+@$hostname}.sqlite3
}

## @var _ble_histdb_file
_ble_histdb_file=

## @arr _ble_histdb_bgproc=(fd_response fd_request - bgpid)
##
_ble_histdb_bgproc=()
_ble_histdb_bgproc_fname=()
_ble_histdb_timeout=5000
_ble_histdb_keepalive=1800 # sec

function ble/histdb/escape-for-sqlite-glob {
  ret=$1
  if [[ $ret == *["[*?"]* ]]; then
    local a b
    for a in '[' '*' '?'; do
      b=[$a] ret=${ret//"$a"/"$b"}
    done
  fi
}

function ble/histdb/sqlite3.flush {
  if [[ ${_ble_histdb_bgproc[0]} && _ble_bash -ge 40000 ]]; then
    local line IFS=
    ble/bash/read-timeout 0.001 -r -d '' line <&"${_ble_histdb_bgproc[0]}"
  fi
}
function ble/histdb/sqlite3.request {
  ble/histdb/sqlite3.open
  if [[ ${_ble_histdb_bgproc[1]} ]]; then
    ble/histdb/sqlite3.flush
    ble/util/print "$1" >&"${_ble_histdb_bgproc[1]}"
  else
    [[ $1 == .exit ]] && return 0
    "$_ble_histdb_sqlite3" -quote "$_ble_histdb_file" ".timeout $_ble_histdb_timeout" "$1"
  fi
}

function ble/histdb/read-single-value {
  local line nl=$'\n' q=\' qq=\'\' Q="'\''"
  local IFS=
  if ble/bash/read line && [[ $line == \'* ]]; then
    local out=$line ext=0
    while ((ext==0)) && ! ble/string#match "$out" '^'"$q"'([^'\'']|'"$qq"')*'"$q"'$'; do
      ble/bash/read line; ext=$?
      out=$out$nl$line
    done
    line=$out
  fi
  line=${line#$q}
  line=${line%$q}
  line=${line//$qq/$q}
  line=$q${line//$q/$Q}$q
  ble/util/print "ret=$line"
}

function ble/histdb/sqlite3.request-single-value {
  local query=$1 opts=$2
  ble/histdb/sqlite3.open
  if [[ ${_ble_histdb_bgproc[1]} && _ble_bash -ge 40000 ]]; then
    ble/histdb/sqlite3.flush
    ble/util/print "$query" >&"${_ble_histdb_bgproc[1]}"

    local sync_command='ble/histdb/read-single-value <&"${_ble_histdb_bgproc[0]}"'
    local sync_condition='((sync_elapsed<200)) || ! ble/complete/check-cancel'
    local sync_opts=progressive-weight
    [[ :$opts: == *:auto:* && $bleopt_complete_timeout_auto ]] &&
      sync_opts=$sync_opts:timeout=$((bleopt_complete_timeout_auto))
    ble/util/assign ret 'ble/util/conditional-sync "$sync_command" "$sync_condition" "" "$sync_opts"' &>/dev/null; local ext=$?
    builtin eval -- "$ret"
    ((ext==142)) && ext=148
    return "$ext"
  else
    local out q=\' qq=\'\'
    ble/util/assign out '"$_ble_histdb_sqlite3" -quote "$_ble_histdb_file" ".timeout $_ble_histdb_timeout" "$query"'
    out=${out#$q}
    out=${out%$q}
    ret=${out//$qq/$q}
  fi
}

function ble/histdb/sqlite3.proc {
  exec "$_ble_histdb_sqlite3" -quote -cmd "-- [ble: $BLE_SESSION_ID]" "$_ble_histdb_file"
}

_ble_histdb_keepalive_enabled=
ble/is-function ble/util/idle.push &&
  _ble_histdb_keepalive_enabled=1

## @fn ble/histdb/sqlite3.open
##   @var[ref] _ble_histdb_file
##   @arr[out] _ble_histdb_bgproc
##   @arr[out] _ble_histdb_bgproc_fname
function ble/histdb/sqlite3.open {
  if [[ $_ble_histdb_file ]]; then
    # background process に依らないモードの時は OK
    ble/util/bgproc#opened _ble_histdb || return 0

    ble/util/bgproc#alive _ble_histdb; local state=$?
    if ((state==0)); then
      ble/util/bgproc#keepalive _ble_histdb
      return 0
    else
      if ((state==3)); then
        # background process が死んでいる時は再度開き直す。
        ble/util/print 'histdb: background sqlite3 is inactive.' >&2
      fi
      ble/util/bgproc#start _ble_histdb
      return "$?"
    fi
  fi

  local ret; ble/histdb/.get-filename
  _ble_histdb_file=$ret

  local session_id=$_ble_base_session
  local start_time=${session_id%%/*}

  IFS=, builtin eval 'local groups="${GROUPS[*]}"'
  local user=${USER:-$_ble_base_env_USER}
  local hostname=${HOSTNAME:-$_ble_base_env_HOSTNAME}

  local screen_info= ssh_info=
  [[ $STY ]] && screen_info="$STY $WINDOW"
  [[ $SSH_TTY ]] && ssh_info="$SSH_TTY $SSH_CONNECTION"

  local bgproc_opts=owner-close-on-unload:kill-timeout=60000
  [[ $_ble_histdb_keepalive_enabled ]] &&
    bgproc_opts=$bgproc_opts:timeout=$((_ble_histdb_keepalive*1000))
  if ble/util/bgproc#open _ble_histdb ble/histdb/sqlite3.proc "$bgproc_opts"; (($?!=0&&$?!=3)); then
    # Note: システムが元々サポートしていない場合 ($? == 3) はエラーメッセージは
    # 出さない。
    local msg='[ble histdb: background sqlite3 failed to start]'
    ble/util/print "${_ble_term_setaf[9]}$msg$_ble_term_sgr0" >&2
  fi

  local ret q=\' qq=\'\'
  ble/histdb/sqlite3.request-single-value "
    BEGIN IMMEDIATE TRANSACTION;
    CREATE TABLE IF NOT EXISTS misc(key TEXT PRIMARY KEY, value INTEGER);
    INSERT OR IGNORE INTO misc values('version', $_ble_histdb_version);
    CREATE TABLE IF NOT EXISTS sessions(
      session_id TEXT PRIMARY KEY, pid INTEGER, ppid INTEGER,
      hostname TEXT, user TEXT, uid INTEGER, euid INTEGER, groups TEXT,
      start_time INTEGER, start_wd TEXT,
      bash_path TEXT, bash_version TEXT, shlvl INTEGER,
      blesh_path TEXT, blesh_version TEXT,
      term TEXT, lang TEXT, display TEXT, screen_info TEXT, ssh_info TEXT,
      tty TEXT, last_time INTEGER, last_wd TEXT);
    CREATE TABLE IF NOT EXISTS command_history(
      session_id TEXT, command_id INTEGER,
      lineno INTEGER, history_index INTEGER,
      command TEXT, cwd TEXT, inode INTEGER, issue_time INTEGER,
      status             INTEGER,
      pipestatus         TEXT,
      lastarg            TEXT,
      bgpid              INTEGER,
      exec_time          INTEGER,
      exec_time_beg      INTEGER,
      exec_time_end      INTEGER,
      exec_time_real     INTEGER,
      exec_time_usr      INTEGER,
      exec_time_sys      INTEGER,
      exec_time_cpu      INTEGER,
      exec_time_usr_self INTEGER,
      exec_time_sys_self INTEGER,
      exec_time_usr_chld INTEGER,
      exec_time_sys_chld INTEGER,
      remarks            TEXT,
      PRIMARY KEY (session_id, command_id));
    CREATE TABLE IF NOT EXISTS words(
      word TEXT PRIMARY KEY, count INTEGER, ctime INTEGER, mtime INTEGER);
    INSERT OR IGNORE INTO
      sessions(
        session_id, pid, ppid,
        hostname, user, uid, euid, groups,
        start_time, start_wd,
        bash_path, bash_version, shlvl,
        blesh_path, blesh_version,
        term, lang, display, screen_info, ssh_info,
        tty)
      VALUES(
        '${session_id//$q/$qq}', '${$//$q/$qq}', '${PPID//$q/$qq}',
        '${hostname//$q/$q}', '${user//$q/$qq}', '${UID//$q/$qq}', '${EUID/$q/$qq}', '${groups//$q/$qq}',
        '${start_time//$q/$qq}', '${PWD//$q/$qq}',
        '${BASH//$q/$qq}', '${BASH_VERSION//$q/$qq}', '${SHLVL//$q/$qq}',
        '${_ble_base_blesh//$q/$qq}', '${BLE_VERSION//$q/$qq}',
        '${TERM//$q/$qq}', '${LANG//$q/$qq}', '${DISPLAY//$q/$qq}', '${screen_info//$q/$qq}', '${ssh_info//$q/$qq}',
        '${_ble_prompt_const_l//$q/$qq}');
    COMMIT;
    SELECT VALUE FROM misc WHERE key = 'version';"
  local version=$ret

  if [[ $version ]] && ((version<_ble_histdb_version)); then
    local query=
    ((version<2)) &&
      query=$query$_ble_term_nl"ALTER TABLE command_history ADD COLUMN inode INTEGER;"
    ((version<3)) &&
      query=$query$_ble_term_nl"ALTER TABLE command_history ADD COLUMN remarks TEXT;"
    query=$query$_ble_term_nl"UPDATE misc SET value = $_ble_histdb_version WHERE key = 'version';"
    ble/histdb/sqlite3.request "$query"
  fi
}

function ble/util/bgproc/onstart:_ble_histdb {
  ble/util/bgproc#post _ble_histdb ".timeout $_ble_histdb_timeout"
}

function ble/util/bgproc/onstop:_ble_histdb {
  ble/util/bgproc#post _ble_histdb '.exit'
}

function ble/histdb/sqlite3.close {
  [[ $_ble_histdb_file ]] || return 0
  local session_id=$_ble_base_session ret
  ble/util/time; local time=$ret
  ble/histdb/sqlite3.request "
    UPDATE sessions SET
      last_time = '${time//$q/$qq}',
      last_wd = '${PWD//$q/$qq}'
      WHERE session_id = '${session_id//$q/$qq}';"

  ble/util/bgproc#close _ble_histdb
  _ble_histdb_file=
}

_ble_histdb_exec_ignore=()
_ble_histdb_exec_cwd=
_ble_histdb_exec_cwd_inode=

function ble/histdb/update-cwd-inode {
  [[ $PWD == "$_ble_histdb_exec_cwd" ]] && return 0

  _ble_histdb_exec_cwd=$PWD
  _ble_histdb_exec_cwd_inode=
  if local ret; ble/file#inode "$PWD" && ble/string#match "$ret" '^[0-9]+$'; then
    _ble_histdb_exec_cwd_inode=$ret
  fi
}

function ble/histdb/collect-words.proc {
  [[ $wtype && ! ${wtype//[0-9]} ]] &&
    ble/array#push collect_words "${_ble_edit_str:wbeg:wlen}"
}
function ble/histdb/collect-words {
  ble-edit/content/update-syntax
  local -a collect_words=()
  ble/syntax/tree-enumerate-in-range 0 "${#_ble_edit_str}" ble/histdb/collect-words.proc
  ble/array#reverse collect_words

  ret=()
  local word
  "${_ble_util_set_declare[@]//NAME/mark}" # WA #D1570 checked
  for word in "${collect_words[@]}"; do
    # Note (#D1958): Even if the same words appeared in a single command, we
    #   only count 1 for one command.  This is because the "words" table is
    #   used for the word completion, where the count is used as an importance
    #   measure.  We count the number of commands where the word appears
    #   because the same word can easily appear in a single command multiple
    #   times.
    ble/set#contains mark "$word" && return 0

    ble/set#add mark "$word"
    ble/array#push ret "$word"
  done
}

## @fn ble/histdb/exec_register.hook command
##   @var[in] command_id
##   @var[in] lineno
##     これらの変数は exec_register の側で用意される。
function ble/histdb/exec_register.hook {
  local command=$1
  if [[ $bleopt_histdb_ignore ]]; then
    local patterns pattern
    ble/string#split patterns : "$bleopt_histdb_ignore"
    for pattern in "${patterns[@]}"; do
      if [[ $command == $pattern ]]; then
        _ble_histdb_exec_ignore[command_id]=1
        return 0
      fi
    done
  fi
  builtin unset -v '_ble_histdb_exec_ignore[command_id]'

  local q=\' qq=\'\'
  local session_id=$_ble_base_session ret
  ble/util/time; local issue_time=$ret

  # @var history_index ... history index: The current command might not be
  # registered to the command history, but we always pick up the index of the
  # last entry because there is no way to check it reliably.  We could compare
  # the top element with BASH_COMMAND, but the history entry might be
  # transformed by HISTCONTROL=strip, etc.
  local history_index; ble/history/get-count -v history_index
  ((history_index++))

  # Expand "bleopt histdb_remarks"
  local remarks
  ble/histdb/.get-remarks

  local ret word extra_query=
  ble/histdb/collect-words
  for word in "${ret[@]}"; do
    extra_query=$extra_query"INSERT OR REPLACE INTO words(word, count, ctime, mtime)
      VALUES('${word//$q/$qq}',
      coalesce((SELECT count FROM words WHERE word = '${word//$q/$qq}'), 0) + 1,
      coalesce((SELECT ctime FROM words WHERE word = '${word//$q/$qq}'), $issue_time),
      $issue_time);"
  done

  ble/histdb/update-cwd-inode
  local inode=$_ble_histdb_exec_cwd_inode

  ble/histdb/sqlite3.request "
    BEGIN IMMEDIATE TRANSACTION;
    UPDATE sessions SET
      last_time = '${issue_time//$q/$qq}',
      last_wd = '${PWD//$q/$qq}'
      WHERE session_id = '${session_id//$q/$qq}';
    -- Update duplicate command_id
    UPDATE command_history SET
      command_id = min(0, (SELECT command_id FROM command_history WHERE session_id = '${session_id//$q/$qq}')) - 1
      WHERE session_id = '${session_id//$q/$qq}' AND command_id = $command_id;
    -- Add command_history entry
    INSERT INTO command_history(
        session_id, command_id,
        lineno, history_index,
        command, cwd, inode, issue_time, remarks)
      VALUES(
        '${session_id//$q/$qq}', '${command_id//$q/$qq}',
        '${lineno//$q/$qq}', '${history_index//$q/$qq}',
        '${command//$q/$qq}', '${PWD//$q/$qq}', ${inode:-NULL}, '${issue_time//$q/$qq}', '${remarks//$q/$qq}');
    $extra_query
    COMMIT;"
}

function ble/histdb/postexec.hook {
  local session_id=$_ble_base_session
  local command_id=$_ble_edit_exec_command_id
  if [[ ${_ble_histdb_exec_ignore[command_id]+set} ]]; then
    builtin unset -v '_ble_histdb_exec_ignore[command_id]'
    return 0
  fi

  local ret
  ble/util/time; local last_time=$ret

  IFS=, builtin eval 'local pipestatus="${_ble_edit_exec_PIPESTATUS[*]}"'
  local lastarg=$_ble_edit_exec_lastarg
  ((${#lastarg}>=1000)) && lastarg=${lastarg::997}...

  local real=$_ble_exec_time_tot
  local usr=$_ble_exec_time_usr
  local sys=$_ble_exec_time_sys
  local cpu=0; ((real>0)) && cpu=$(((usr+sys)/real))
  local usr_chld=$((usr-_ble_exec_time_usr_self))
  local sys_chld=$((sys-_ble_exec_time_sys_self))

  local q=\' qq=\'\'
  ble/histdb/sqlite3.request "
    UPDATE sessions SET
      last_time = '${last_time//$q/$qq}',
      last_wd = '${PWD//$q/$qq}'
      WHERE session_id = '${session_id//$q/$qq}';
    UPDATE command_history SET
      status             = '${_ble_edit_exec_lastexit//$q/$qq}',
      pipestatus         = '${pipestatus//$q/$qq}',
      lastarg            = '${lastarg//$q/$qq}',
      bgpid              = '${!//$q/$qq}',
      exec_time          = '${_ble_exec_time_ata//$q/$qq}',
      exec_time_beg      = '${_ble_exec_time_beg//$q/$qq}',
      exec_time_end      = '${_ble_exec_time_end//$q/$qq}',
      exec_time_real     = '${real//$q/$qq}',
      exec_time_usr      = '${usr//$q/$qq}',
      exec_time_sys      = '${sys//$q/$qq}',
      exec_time_cpu      = '${cpu//$q/$qq}',
      exec_time_usr_self = '${_ble_exec_time_usr_self//$q/$qq}',
      exec_time_sys_self = '${_ble_exec_time_sys_self//$q/$qq}',
      exec_time_usr_chld = '${usr_chld//$q/$qq}',
      exec_time_sys_chld = '${sys_chld//$q/$qq}'
      WHERE session_id = '${session_id//$q/$qq}' AND command_id = $command_id;"
}

function ble/histdb/backup {
  local file=$1
  ble/bin#freeze-utility-path -n gzip || return 1

  local backup=${file%.sqlite3}.backup.sqlite3 q=\' qq=\'\'
  if [[ -s $backup.gz ]]; then
    # If there is already an up-to-date backup file, we skip the backup.
    local ret now
    ble/util/time; now=$ret
    ble/file#mtime "$backup.gz" || return 1
    ((now>ret+24*3600)) || return 1
  fi

  "$_ble_histdb_sqlite3" "$file" \
    ".timeout $_ble_histdb_timeout" \
    "PRAGMA quick_check;" \
    ".backup '${backup//$q/$qq}'" \
    "ATTACH DATABASE '${backup//$q/$qq}' AS backup;
    PRAGMA backup.quick_check;" >/dev/null &&
    ble/bin/gzip -c "$backup" >| "$backup.gz.part" &&
    [[ -s $backup.gz.part ]] &&
    ble/bin/mv -f "$backup.gz.part" "$backup.gz" &&
    ble/bin/rm -f "$backup"
}

function ble/histdb/unload.hook {
  if [[ $_ble_histdb_file ]]; then
    local file=$_ble_histdb_file
    ble/histdb/sqlite3.close
    ble/histdb/backup "$file"
  fi
}

# 設定が変化して記録先の history.sqlite3 のパスが変わったら現在のファイルは閉じ
# る。必要になった時に初めて新しいファイルを開く。
function ble/histdb/exec_end.hook {
  local ret; ble/histdb/.get-filename
  [[ $ret == "$_ble_histdb_file" ]] ||
    ((${#_ble_edit_exec_lines[@]})) ||
    ble/histdb/sqlite3.close
}

blehook exec_register!=ble/histdb/exec_register.hook
blehook POSTEXEC!=ble/histdb/postexec.hook
blehook exec_end!=ble/histdb/exec_end.hook
blehook unload!=ble/histdb/unload.hook

#------------------------------------------------------------------------------
# auto-complete

function ble/complete/auto-complete/source:histdb-history {
  [[ $_ble_history_prefix ]] && return 1

  local limit=$((bleopt_line_limit_length)) limit_condition=
  if ((limit)); then
    ((limit-=${#_ble_edit_str},limit>0)) || return 1
    limit_condition=" AND length(command) <= $limit"
  fi

  local ret
  ble/histdb/escape-for-sqlite-glob "$_ble_edit_str"; local pat=$ret?*

  ble/histdb/update-cwd-inode
  local inode=$_ble_histdb_exec_cwd_inode

  local q=\' qq=\'\'
  ble/histdb/sqlite3.request-single-value "
    SELECT coalesce(
      (SELECT command FROM (SELECT command, max(issue_time) FROM command_history WHERE command GLOB '${pat//$q/$qq}' AND cwd = '${PWD//$q/$qq}$limit_condition')),
      ${inode:+(SELECT command FROM (SELECT command, max(issue_time) FROM command_history WHERE command GLOB '${pat//$q/$qq}' AND inode = $inode$limit_condition)),}
      '');"
  [[ $ret == "$_ble_edit_str"?* ]] || return 1
  ble/complete/auto-complete/enter h 0 "${ret:${#_ble_edit_str}}" '' "$ret"
}

function ble/complete/auto-complete/source:histdb-word {
  [[ $_ble_history_prefix ]] && return 1

  local iN=${#_ble_edit_str}
  ((_ble_edit_ind>0)) || return 1

  local limit=$((bleopt_line_limit_length)) limit_condition=
  if ((limit)); then
    ((limit-=iN,limit>0)) || return 1
    limit_condition=" AND length(word) <= $limit"
  fi

  local -a wbegins=()
  if ((_ble_edit_ind<iN)); then
    # 行中にいる時は現在位置で完結している単語だけを対象にする。
    if [[ ${_ble_syntax_tree[_ble_edit_ind-1]} ]]; then
      local tree
      ble/string#split-words tree "${_ble_syntax_tree[_ble_edit_ind-1]}"
      local wtype=${tree[0]} wlen=${tree[1]}
      [[ $wtype && ! ${wtype//[0-9]} && wlen -ge 0 ]] &&
        ble/array#push wbegins "$((_ble_edit_ind-wlen))"
    elif local ret; ble/syntax/completion-context/.search-last-istat "$((_ble_edit_ind-1))"; then
      local istat=$ret stat wlen
      ble/string#split-words stat "${_ble_syntax_stat[istat]}"
      if (((wlen=stat[1])>=0)); then
        ble/array#push wbegins "$((istat-wlen))"
      elif [[ ${_ble_syntax_bash_command_EndWtype[stat[0]]:-} ]]; then
        # 単語の始まりの時 (_ble_syntax_bash_command_EndWtype で判定できるのかは実は非自明)
        ble/array#push wbegins "$istat"
      fi
    fi

  else
    # 閉じていない入れ子に対する列挙

    local -a stat nest tree
    ble/string#split-words stat "${_ble_syntax_stat[iN]}"
    local wlen tclen
    if (((wlen=stat[1])>=0)); then
      ble/array#push wbegins "$((iN-wlen))"
    elif (((tclen=stat[4])>=0)); then
      local wend=$((iN-tclen))
      ble/string#split-words tree "${_ble_syntax_tree[wend-1]}"
      local wtype=${tree[0]} wlen=${tree[1]}
      [[ $wtype && ! ${wtype//[0-9]} && wlen -ge 0 ]] &&
        ble/array#push wbegins "$((wend-wlen))"
    fi

    local inest=$iN nlen=${stat[3]}
    while ((nlen>0)); do
      ((inest-=nlen))
      ble/string#split-words nest "${_ble_syntax_nest[inest]}"
      (((wlen=nest[1])>=0)) &&
        ble/array#push wbegins "$((inest-wlen))"
      nlen=${nest[3]}
    done
  fi
  ((${#wbegins[@]})) || return 1

  local -a sqls=()

  local i q=\' qq=\'\'
  for i in "${wbegins[@]}"; do
    local word=${_ble_edit_str:i:_ble_edit_ind-i}
    [[ $word ]] || continue
    local ret; ble/histdb/escape-for-sqlite-glob "$word"
    local pat=$ret?*
    ble/array#push sqls "SELECT '$i:' || word FROM (SELECT word, max(mtime) FROM words WHERE word GLOB '${pat//$q/$qq}'$limit_condition)"
  done
  ((${#sqls[@]})) || return 1

  ble/array#reverse sqls
  sqls=("${sqls[@]/#/(}") # WA #D1570 checked
  sqls=("${sqls[@]/%/)}") # WA #D1570 checked
  ble/array#push sqls "''"
  IFS=, builtin eval 'local query="select coalesce(${sqls[*]});"'

  local ret
  ble/histdb/sqlite3.request-single-value "$query" auto || return 1
  [[ $ret == *:* ]] || return 1

  local index=${ret%%:*} insert=${ret#*:}
  if local comps=${_ble_edit_str:index:_ble_edit_ind-index}; [[ $insert == "$comps"* ]]; then
    local ins=${insert:_ble_edit_ind-index}
    ble/complete/auto-complete/enter c "$_ble_edit_ind" "$ins" "$insert" "$ins" "$ins" ' '
  else
    ble/complete/auto-complete/enter r "$index" " [$insert] " "$insert" "$insert" "$insert" ' '
  fi
}
ble/util/import/eval-after-load core-complete '
  ble/array#insert-before _ble_complete_auto_source history histdb-history
  ble/array#push _ble_complete_auto_source histdb-word'
[[ $_ble_histdb_keepalive_enabled ]] &&
  ble/util/idle.push ble/histdb/sqlite3.open

#------------------------------------------------------------------------------
# ble histdb command

function ble-histdb {
  builtin eval -- "$_ble_bash_POSIXLY_CORRECT_local_adjust"
  local set shopt ext
  ble/base/.adjust-bash-options set shopt
  if (($#==0)); then
    ble/histdb/sub:query 'select command from command_history;'
    ext=$?
  elif ble/is-function ble/histdb/sub:"$1"; then
    ble/histdb/sub:"$@"
    ext=$?
  else
    builtin printf 'ble-histdb: unknown command "%s"\n' "$1"
    ext=2
  fi
  ble/base/.restore-bash-options set shopt
  ble/util/setexit "$ext"
  builtin eval -- "$_ble_bash_POSIXLY_CORRECT_local_return"
}

# 一般的な補完のフレームワークを作っても良いのではないかという気がしてきたが、
# それは後で。
function ble/cmdinfo/complete:ble-histdb {
  if ((comp_cword==1)); then
    local ret sub
    ble/util/compgen ret -A function -- 'ble/histdb/sub:'
    ((${#ret[@]})) || return 0

    local cands
    for sub in "${ret[@]#ble/histdb/sub:}"; do # disable=#D2352
      if [[ $sub != */* && $sub == "$COMPV"* ]]; then
        ble/array#push cands "$sub"
      fi
    done

    if ((${#cands[@]})); then
      local "${_ble_complete_yield_varnames[@]/%/=}" # disable=#D1570
      ble/complete/cand/yield.initialize word
      ble/complete/cand/yield.batch word
    fi
  fi
}

function ble/histdb/sub:query {
  local ret
  ble/histdb/.get-filename; local histdb_file=$ret
  "$_ble_histdb_sqlite3" "$histdb_file" '.timeout 1000' "$@"
}

#------------------------------------------------------------------------------

function ble/histdb/count2si {
  local si
  si=('' k M G T P E Z Y R Q)

  local count=$1 i
  for ((i=0;i<${#si[@]}-1;i++,count/=1000)); do
    if ((count<1000)); then
      ret=$count${si[i]}
      return 0
    elif ((count<100000)); then
      count=${count%???}.${count:${#count}-3}
      ret=${count::4}${si[i+1]}
      return 0
    fi
  done
  ret=$count${si[${#si[@]}-1]}
}

# modified ble/contrib/prompt-elapsed/output-sec.ps
function ble/histdb/usec2s {
  local usec=${1:-0}
  if ((usec<1000)); then
    ret=${usec}us
  elif ((usec<100000)); then
    usec=${usec%???}.${usec:${#usec}-3}
    ret=${usec::4}ms
  else
    ble/histdb/msec2s "$((usec/1000))"
  fi
}

function ble/histdb/msec2s {
  local msec=${1:-0}
  if ((msec<1000)); then
    ret=${msec}ms
  elif ((msec<100000)); then
    msec=${msec%???}.${msec:${#msec}-3}
    ret=${msec::4}s
  else
    ble/histdb/sec2s "$((msec/1000))"
  fi
}

function ble/histdb/sec2s {
  local sec=${1:-0} min
  ((min=sec/60,sec%=60))
  if ((min<100)); then
    ret=${min}m${sec}s
    return 0
  fi

  local hour; ((hour=min/60,min%=60))
  if ((hour<100)); then
    ret="${hour}h:${min}m:${sec}s"
    return 0
  fi

  local day; ((day=hour/24,hour%=24))
  if ((day<365)); then
    ret="${day}d-${hour}h:${min}m"
    return 0
  fi

  local year; ((year=day/365,day%=365))
  ret="${year}y-${day}d-${hour}h"
  return 0
}

#------------------------------------------------------------------------------
# usage: ble histdb stats

bleopt/declare -v histdb_stats_items 'commands unique-commands successful-commands exec-time exec-cpu-time words sessions directories'

function ble/histdb/sub:stats/get-user-name {
  ble/bin#has git && ble/util/assign ret '
    git config user.name 2>/dev/null
  ' && [[ $ret ]] && return 0

  ble/bin#has getenv && ble/util/assign ret '
    getent passwd 2>/dev/null | ble/bin/awk -F : -v UID="$UID" '\''
      $3==UID {
        sub(/[[:blank:]]*[<>].*$/, "", $5);
        print $5;
      }
    '\''
  ' && [[ $ret ]] && return 0

  ret=${USER:-$_ble_base_env_USER}@${HOSTNAME:-$_ble_base_env_HOSTNAME}
}

## @fn ble/histdb/sub:stats/where.check condition
##   @param[in] condition
##   @var[ref] where
function ble/histdb/sub:stats/where.check {
  where="${where:- WHERE}${where:+ AND} $1"
}
## @fn ble/histdb/sub:stats/where.check-datetime column
##   @param[in] column
##   @var[in] time_beg time_end
##   @var[ref] where
function ble/histdb/sub:stats/where.check-datetime {
  [[ $time_beg ]] && ble/histdb/sub:stats/where.check "$1 >= $time_beg"
  [[ $time_end ]] && ble/histdb/sub:stats/where.check "$1 < $time_end"
}

function ble/histdb/sub:stats/item:commands {
  case $1 in
  (init)
    local where=
    ble/histdb/sub:stats/where.check-datetime issue_time
    ble/array#push queries "SELECT count(*) FROM command_history$where;" ;;
  (get)
    ble/array#push names  'Total commands'
    local ret
    ble/histdb/count2si "$2"
    ble/array#push values "$ret" ;;
  esac
}

function ble/histdb/sub:stats/item:unique-commands {
  case $1 in
  (init)
    local where=
    ble/histdb/sub:stats/where.check-datetime issue_time
    ble/array#push queries "SELECT count(*) FROM (SELECT DISTINCT command FROM command_history$where);" ;;
  (get)
    ble/array#push names  'Total unique commands'
    local ret
    ble/histdb/count2si "$2"
    ble/array#push values "$ret" ;;
  esac
}

function ble/histdb/sub:stats/item:successful-commands {
  case $1 in
  (init)
    local where=
    ble/histdb/sub:stats/where.check 'status == 0'
    ble/histdb/sub:stats/where.check-datetime issue_time
    ble/array#push queries "SELECT count(*) FROM (SELECT command FROM command_history$where);" ;;
  (get)
    ble/array#push names  'Total successful commands'
    local ret
    ble/histdb/count2si "$2"
    ble/array#push values "$ret" ;;
  esac
}

function ble/histdb/sub:stats/item:sessions {
  case $1 in
  (init)
    local where=
    if [[ $time_end ]]; then
      ble/histdb/sub:stats/where.check-datetime start_time
    else
      ble/histdb/sub:stats/where.check-datetime last_time
    fi
    ble/array#push queries "SELECT count(*) FROM sessions$where;" ;;
  (get)
    ble/array#push names  'Total sessions'
    local ret
    ble/histdb/count2si "$2"
    ble/array#push values "$ret" ;;
  esac
}

function ble/histdb/sub:stats/item:words {
  case $1 in
  (init)
    local where=
    if [[ $time_end ]]; then
      ble/histdb/sub:stats/where.check-datetime ctime
    else
      ble/histdb/sub:stats/where.check-datetime mtime
    fi
    ble/array#push queries "SELECT count(*) FROM words$where;" ;;
  (get)
    ble/array#push names  'Total distinct words'
    local ret
    ble/histdb/count2si "$2"
    ble/array#push values "$ret" ;;
  esac
}

function ble/histdb/sub:stats/item:directories {
  case $1 in
  (init)
    local where=
    ble/histdb/sub:stats/where.check-datetime issue_time
    ble/array#push queries "SELECT count(*) FROM (SELECT DISTINCT cwd FROM command_history$where);" ;;
  (get)
    ble/array#push names  'Total directories'
    local ret
    ble/histdb/count2si "$2"
    ble/array#push values "$ret" ;;
  esac
}

function ble/histdb/sub:stats/item:exec-time {
  case $1 in
  (init)
    local where=
    ble/histdb/sub:stats/where.check-datetime issue_time
    ble/array#push queries "SELECT sum(exec_time) FROM command_history$where;" ;;
  (get)
    ble/array#push names  'Total execution time'
    local ret
    ble/histdb/usec2s "$2"
    ble/array#push values "$ret" ;;
  esac
}

function ble/histdb/sub:stats/item:exec-cpu-time {
  case $1 in
  (init)
    local where=
    ble/histdb/sub:stats/where.check-datetime issue_time
    ble/array#push queries "SELECT sum(exec_time_usr + exec_time_sys) FROM command_history$where;" ;;
  (get)
    ble/array#push names  'Total CPU time'
    local ret
    ble/histdb/msec2s "$2"
    ble/array#push values "$ret" ;;
  esac
}

function ble/histdb/sub:stats {
  local period=${1-}

  local time_beg= time_end=

  if [[ $period ]]; then
    local ret
    if ble/string#match "$period" '^([0-9]+)([smhdMy])$'; then
      local delta=${BASH_REMATCH[1]}
      case ${BASH_REMATCH[2]} in
      (m) ((delta*=60)) ;;
      (h) ((delta*=3600)) ;;
      (d) ((delta*=3600*24)) ;;
      (M) ((delta*=3600*24*30)) ;;
      (y) ((delta*=3600*24*365)) ;;
      esac
      ble/util/time
      ((time_beg=ret-delta))
    elif ble/string#match "$period" '^2[0-9]{3}(-(0[1-9]|1[0-2])(-(0[1-9]|[12][0-9]|3[01]))?)?$'; then
      local y=${period::4} m=${BASH_REMATCH[2]-} d=${BASH_REMATCH[4]-}
      if [[ $d ]]; then
        m=${m#0} d=${d#0}
        ble/util/mktime "$y-$m-$d 00:00:00"
        time_beg=$ret
        # Note: we here assume that 01-32 is equivalent to 02-01, etc.
        ble/util/mktime "$y-$m-$((d+1)) 00:00:00"
        time_end=$ret
      elif [[ $m ]]; then
        m=${m#0}
        ble/util/mktime "$y-$m-01 00:00:00"
        time_beg=$ret
        ble/util/mktime "$((y+m/12))-$((m%12+1))-01 00:00:00"
        time_end=$ret
      else
        ble/util/mktime "$y-01-01 00:00:00"
        time_beg=$ret
        ble/util/mktime "$((y+1))-01-01 00:00:00"
        time_end=$ret
      fi
    else
      ble/util/print "ble-histdb-stats: unrecognized argument '$period'" 2>/dev/null
      return 2
    fi
  fi

  local -a queries=()
  local items item
  ble/string#split-words items "$bleopt_histdb_stats_items"
  for item in "${items[@]}"; do
    ble/histdb/sub:stats/item:"$item" init
  done

  local IFS=$_ble_term_IFS
  ble/histdb/sub:query -separator ' ' "${queries[*]}" | {
    local result
    ble/util/mapfile result

    local ret sgr0=$_ble_term_sgr0
    ble/color/gspec2sgr 'fg=27,bold'; local sgr1=$ret
    ble/histdb/sub:stats/get-user-name
    ble/util/print "$sgr1$ret's Shell Stats${period:+ ($period)}$sgr0"

    local -a names=() values=()
    local i=0
    for item in "${items[@]}"; do
      ble/histdb/sub:stats/item:"$item" get "${result[i++]}"
    done

    local name max_name_width=0
    for name in "${names[@]}"; do
      local w=$((${#name}+1))
      ((max_name_width<w)) && ((max_name_width=w))
    done

    ble/color/gspec2sgr 'bold'; local sgr2=$ret
    for i in "${!names[@]}"; do
      builtin printf '  %s%-*s    %s%s\n' "$sgr2" "$max_name_width" "${names[i]}:" "${values[i]}" "$sgr0"
    done
  }
}

#------------------------------------------------------------------------------
# lib: ble/getopts

## @fn ble/getopts/scan-definition type script
##   @param[in] type
##     Specify the type of
##
##     @fn[opt] ble/getopts/scan-definition:TYPE/label
##     @fn[opt] ble/getopts/scan-definition:TYPE/flag
##     @fn[opt] ble/getopts/scan-definition:TYPE/option
##
##   @param[in] script
##     Define the command-line interface by calling label:, flag:, and option:
##
_ble_getopts_definition_headers=(label flag option arguments)
function ble/getopts/scan-definition {
  local type=$1 def=$2 header fn
  for header in "${_ble_getopts_definition_headers[@]}"; do
    fn=ble/getopts/scan-definition:$type/$header
    if ble/is-function "$fn"; then
      ble/function#push "$header" "$fn \"\$@\""
    else
      ble/function#push "$header" '((1))'
    fi
  done
  builtin eval -- "$def"
  for header in "${_ble_getopts_definition_headers[@]}"; do
    ble/function#pop "$header"
  done
}

function ble/getopts/scan-definition:parser/label {
  getopts_label=$1
}

# todo array
function ble/getopts/scan-definition:parser/option {
  local var=$1 opt=$2 q=\' Q="'\''"

  if [[ $opt == --* ]]; then
    ble/util/print NYI >&2
  elif [[ $opt == -? ]]; then
    opt=${opt:1}
    getopts_short_options=$getopts_short_options'('\'${opt//$q/$Q}\'')
      if [[ ${arg:1} ]]; then
        '$var'=${arg:1} arg=
      elif (($#)); then
        '$var'=$1
        shift
      else
        ble/util/print "$_getopts_label: missing option argument for '\''-$arg'\''" >&2
        flags=E$flags
      fi ;;
    '
  fi
}
function ble/getopts/scan-definition:parser/arguments {
  local var=${1:-args}
  getopts_arguments=$getopts_arguments'
    ble/array#push '$var' "$arg"
  '
}

function ble/getopts/generate-parser {
  local getopts_finalize=
  local getopts_initialize=
  local getopts_label=
  local getopts_long_options=
  local getopts_short_options=
  local getopts_arguments=

  ble/getopts/scan-definition parser "$2"

  if [[ ! $getopts_arguments ]]; then
    getopts_arguments='
      ble/util/print "$_getopt_label: unrecognized argument '\''$arg'\''" >&2
      flags=E$flags
    '
  fi

  local getopts_script='
    local _ble_local_set _ble_local_shopt
    ble/base/.adjust-bash-options _ble_local_set _ble_local_shopt
    local LC_ALL= LC_COLLATE=C 2>/dev/null

    local _getopts_label='$q${getopts_label//$q/$Q}$q'

    flags=
    '$getopts_initialize'
    while (($#)); do
      local arg=$1; shift
      if [[ $arg == -?* && $flags != *-* ]]; then
        if [[ $arg == --* ]]; then
          case $arg in
          (--) flags=-$flags ;;
          '$getopts_long_options'
          (*)
            ble/util/print "$_getopts_label: unrecognized long option '\''$arg'\''" >&2
            flags=E$flags ;;
          esac
        else
          arg=${arg:1}
          while [[ $arg ]]; do
            case ${arg::1} in
            '$getopts_short_options'
            (*)
              ble/util/print "$_getopts_label: unrecognized option '\''-${arg::1}'\''" >&2
              flags=E$flags ;;
            esac
            arg=${arg:1}
          done
        fi
        continue
      fi

      '$getopts_arguments'
    done
    '$getopts_finalize'

    ble/util/unlocal LC_ALL LC_COLLATE 2>/dev/null
    ble/base/.restore-bash-options _ble_local_set _ble_local_shopt
    [[ $flags != *E* ]]
  '

  builtin eval -- "function $1 { $getopts_script }"
}

#------------------------------------------------------------------------------
# lib: ble/histdb/graph

## @fn ble/histdb/graph/board/initialize
##   @arr[out] grboard_export
function ble/histdb/graph/board/initialize {
  local -a colors
  ble/histdb/graph/palette "$2"

  local mark=$1
  if [[ ! $mark ]]; then
    if ble/util/is-unicode-output; then
      mark='■'
    else
      mark='##'
    fi
  fi

  grboard_export=()
  ble/array#push grboard_export "grboard_nlevel=${#colors[@]}"

  local i ret
  for i in "${!colors[@]}"; do
    ble/color/gspec2sgr "fg=${colors[i]}"
    ble/array#push grboard_export "grboard_unit$i=$ret$mark$_ble_term_sgr0"
  done

  local ret
  ble/util/s2w "$mark"; local w=$ret
  ble/string#reserve-prototype "$w"
  ble/array#push grboard_export "grboard_unit_white=${_ble_string_prototype::w}"
}
_ble_histdb_grboard_awk_lib='
  function grboard_initialize(_, i) {
    grboard_nlevel = ENVIRON["grboard_nlevel"];
    grboard_unit[-1] = ENVIRON["grboard_unit_white"];
    for (i = 0; i < grboard_nlevel; i++)
      grboard_unit[i] = ENVIRON["grboard_unit" i];
  }
  function grboard__score2unit(score, score_max) {
    if (score == "" || score < 0)
      return grboard_unit[-1];
    else if (score == 0)
      return grboard_unit[0];
    else
      return grboard_unit[1 + int((grboard_nlevel - 1) * score / (score_max + 1))];
  }
  function grboard_render(lines, table, w, h, _, x, y, maxv, v, line) {
    maxv = 0;
    for (y = 0; y < h; y++)
      for (x = 0; x < w; x++)
        if (table[y, x] > maxv)
          maxv = table[y, x];

    for (y = 0; y < h; y++) {
      line = "";
      for (x = 0; x < w; x++) {
        v = table[y, x];
        line = line grboard__score2unit(v, maxv);
      }
      lines[y] = line;
    }
  }
'

## @fn ble/histdb/graph/vbar/initialize [palette]
##   @var[out] grvbar_declare
function ble/histdb/graph/vbar/initialize {
  local -a colors
  ble/histdb/graph/palette "$1"
  local gspec=${colors[0]}

  grvbar_declare=()

  local ret sgr1= sgr0=
  if ble/util/is-unicode-output; then
    local nlevel=8
    if [[ $gspec ]]; then
      ble/color/gspec2sgr "fg=$gspec"
      sgr1=$ret sgr0=$_ble_term_sgr0
    fi
    ble/util/c2w "$((0x2581))"; local w=$ret
    ble/array#push grvbar_declare grvbar_nlevel="$nlevel" grvbar_unit0='  '
    local i
    for ((i=1;i<=nlevel;i++)); do
      ble/util/c2s "$((0x2580+i))"
      ((w==1)) && ret=$ret$ret
      ble/array#push grvbar_declare "grvbar_unit$i=$sgr1$ret$sgr0"
    done
  else
    if [[ $gspec ]]; then
      ble/color/gspec2sgr "bg=$gspec"
      sgr1=$ret sgr0=$_ble_term_sgr0
    fi
    ble/array#push grvbar_declare grvbar_nlevel=1 grvbar_unit0='  ' grvbar_unit1="$sgr1  $sgr0"
  fi
}
_ble_histdb_grvbar_awk_lib='
  function grvbar_initialize(_, i) {
    grvbar_nlevel = ENVIRON["grvbar_nlevel"];
    grvbar_unit[-1] = ENVIRON["grvbar_unit_white"];
    for (i = 0; i <= grvbar_nlevel; i++)
      grvbar_unit[i] = ENVIRON["grvbar_unit" i];
  }
  function grvbar__score2unit(score, score_max) {
    if (score <= 0)
      return grvbar_unit[0];
    else
      return grvbar_unit[1 + int((grvbar_nlevel - 1) * score / (score_max + 1))];
  }
  function grvbar__clamp(x, l, u) {
    return x < l ? l : x > u ? u : x;
  }
  function grvbar_render(lines, list, w, h, _, x, y, maxv, line, s) {
    maxv = 0;
    for (x = 0; x < w; x++)
      if (list[x] > maxv)
        maxv = list[x];
    for (y = 0; y < h; y++) {
      line = ""
      for (x = 0; x < w; x++) {
        s = int((grvbar_nlevel * h + 1) * list[x] / (maxv + 1));
        line = line grvbar_unit[grvbar__clamp(s - y * grvbar_nlevel, 0, grvbar_nlevel)];
      }
      lines[h - 1 - y] = line;
    }
  }
'

function ble/histdb/graph/palette {
  local palette=$1

  local index=
  if ble/string#match "$palette" '\[([0-9]+)\]$'; then
    index=${BASH_REMATCH[1]}
    palette=${palette::${#palette}-${#BASH_REMATCH}}
  fi
  if [[ ! $palette ]]; then
    local ret
    ble/util/strftime -v ret '%m'
    case ${ret#0} in
    (10) palette=halloween-light ;;
    (12) palette=winter-light ;;
    (*)  palette=github-light ;;
    esac
  fi

  # https://github.com/orgs/community/discussions/7078
  case $palette in
  (github-light24)    colors=('#ebedf0' '#9be9a8' '#30c463' '#30a14e' '#216e39') ;;
  (github-dark24)     colors=('#161b22' '#0e4429' '#006d32' '#26a641' '#39d353') ;;
  (halloween-light24) colors=('#ebedf0' '#ffee4a' '#ffc501' '#fe9600' '#03001c') ;;
  (halloween-dark24)  colors=('#161b22' '#631c03' '#bd561d' '#fa7a18' '#fddf68') ;;
  (winter-light24)    colors=('#ebedf0' '#b6e3ff' '#54aeff' '#0969da' '#0a3069') ;;
  (winter-dark24)     colors=('#161b22' '#0a3069' '#0969da' '#54aeff' '#b6e3ff') ;;
  (github-light)      colors=(255 151 77 71 23) ;;
  (github-dark)       colors=(234 22 23 35 77) ;;
  (halloween-light)   colors=(255 227 220 208 232) ;;
  (halloween-dark)    colors=(234 52 130 208 221) ;;
  (winter-light)      colors=(255 153 75 26 23) ;;
  (winter-dark)       colors=(234 23 26 75 153) ;;
  (rainbow)
    colors=({196..220..6} {190..46..36}) ;;
  (rainbow2)
    colors=({16..21} {21..45..6} {51..46} {82..190..36} {220..196..6}) ;;
  (*)
    ble/string#split colors , "$z1"
  esac

  # specify one color
  if [[ $index ]]; then
    colors=("${colors[index<${#colors[@]}?index:0]}")
  fi
}

## @fn ble/histdb/graph/hbar/initialize [palette]
##   @var[out] grhbar_declare
function ble/histdb/graph/hbar/initialize {
  local -a colors
  ble/histdb/graph/palette "$1"

  grhbar_declare=()

  local -a sgr=()
  local ret
  if ble/util/is-unicode-output; then
    local nlevel=8
    if ((${#colors[@]})); then
      sgr[0]=$_ble_term_sgr0
      local color
      for color in "${colors[@]}"; do
        ble/color/gspec2sgr "fg=$color"
        ble/array#push sgr "$ret"
      done
    fi

    ble/util/c2w "$((0x2588))"; local w=$ret
    ble/array#push grhbar_declare grhbar_wfactor="$((w==1?2:1))"

    local i unit
    unit[0]=' '
    for ((i=1;i<=nlevel;i++)); do
      ble/util/c2s "$((0x2590-i))"
      ble/array#push unit "$ret"
    done

    ble/array#push grhbar_declare grhbar_unit0=' '
    for ((i=1;i<=nlevel;i++)); do
      ble/array#push grhbar_declare "grhbar_unit$i=${unit[i]}"
    done
    ble/array#push grhbar_declare grhbar_nlevel="$nlevel"
  else
    if ((${#colors[@]})); then
      sgr[0]=$_ble_term_sgr0
      local color
      for color in "${colors[@]}"; do
        ble/color/gspec2sgr "bg=$color"
        ble/array#push sgr "$ret"
      done
    fi
    ble/array#push grhbar_declare grhbar_nlevel=1 grhbar_unit0=' ' grhbar_unit1=' '
    ble/array#push grhbar_declare grhbar_wfactor=2
  fi

  local glevel=$((${#sgr[@]}-1))
  ble/array#push grhbar_declare grhbar_glevel="$glevel"
  for ((i=0;i<=glevel;i++)); do
    ble/array#push grhbar_declare "grhbar_sgr$i=${sgr[i]}"
  done
}
_ble_histdb_grhbar_awk_lib='
  function grhbar_initialize(_, i) {
    grhbar_wfactor = ENVIRON["grhbar_wfactor"];

    grhbar_nlevel = ENVIRON["grhbar_nlevel"];
    for (i = 0; i <= grhbar_nlevel; i++)
      grhbar_unit[i] = ENVIRON["grhbar_unit" i];

    grhbar_glevel = ENVIRON["grhbar_glevel"];
    if (grhbar_glevel > 0) {
      for (i = 0; i <= grhbar_glevel; i++)
        grhbar_sgr[i] = ENVIRON["grhbar_sgr" i];
    } else {
      grhbar_glevel = 0;
      grhbar_sgr[0] = "";
      grhbar_sgr[1] = "";
    }
  }
  function grhbar__score2unit(score, score_max) {
    if (score <= 0)
      return grhbar_unit[0];
    else
      return grhbar_unit[1 + int((grhbar_nlevel - 1) * score / (score_max + 1))];
  }
  function grhbar__clamp(x, l, u) {
    return x < l ? l : x > u ? u : x;
  }
  function grhbar_render(lines, list, w, h, _, x, y, maxv, line, s, sunit, color) {
    w *= grhbar_wfactor;
    maxv = 0;
    for (y = 0; y < w; y++)
      if (list[y] > maxv)
        maxv = list[y];
    for (y = 0; y < h; y++) {
      line = ""
      s = int((grhbar_nlevel * w + 1) * list[y] / (maxv + 1));
      for (x = 0; x < w; x++) {
        sunit = grhbar__clamp(s - x * grhbar_nlevel, 0, grhbar_nlevel);
        color = grhbar_sgr[sunit ? grhbar__clamp(1 + int(grhbar_glevel * x / (w - 1)), 1, grhbar_glevel): 0];
        line = line color grhbar_unit[sunit];
      }
      lines[y] = line grhbar_sgr[0];
    }
  }
'

#------------------------------------------------------------------------------
# usage: ble-histdb top [-x regex|-s regex]

function ble/histdb/sub:top/define {
  label 'ble-histdb-top'
  option exclude -x
  option has_subcommand -s
  option count -c
}
ble/getopts/generate-parser ble/histdb/sub:top/read-arguments ble/histdb/sub:top/define

function ble/histdb/sub:top {
  local flags=
  local -x exclude= has_subcommand= count=20
  ble/histdb/sub:top/read-arguments "$@" || return 2

  local grhbar_declare
  ble/histdb/graph/hbar/initialize rainbow
  local -x "${grhbar_declare[@]}" # disable=#D1566

  local ret
  ble/color/gspec2sgr 'bold'
  local -x sgr_cmd=$ret
  local -x sgr0=$_ble_term_sgr0

  ble-histdb | ble/bin/awk '
    BEGIN {
      has_subcommand = "^(" ENVIRON["has_subcommand"] ")$";
      to_exclude = "^(" ENVIRON["exclude"] ")$";
    }
    {
      sub(/^[[:blank:]]+/, "");
      sub(/^\(\(?/, "& ");
      if ($1 ~ to_exclude) next;

      subcommand = "";
      if ($1 ~ has_subcommand) {
        for (i = 2; i <= NF; i++) {
          if ($i ~ /^-.+/) continue;
          subcommand = $i
          break;
        }
      }

      if (subcommand != "") {
        print $1 " " subcommand;
      } else {
        print $1;
      }
    }
  ' | ble/bin/sort | uniq -c | ble/bin/sort -nr | ble/bin/awk -v count="$count" '
    '"$_ble_histdb_grhbar_awk_lib"'
    BEGIN {
      grhbar_initialize();
      g_entry_count = 0;
      sgr_cmd = ENVIRON["sgr_cmd"];
      sgr0 = ENVIRON["sgr0"];
    }
    {
      i = g_entry_count++;
      g_score[i] = $1;
      g_command[i] = $0;
      if (g_entry_count >= count) exit;
    }
    END {
      grhbar_render(lines_hbar, g_score, 6, g_entry_count);
      for (i = 0; i < g_entry_count; i++) {
        sub(/[0-9]+ /, "&[" lines_hbar[i] "] " sgr_cmd, g_command[i]);
        print g_command[i] sgr0;
      }
    }
  '
}

#------------------------------------------------------------------------------
# usage: ble-histdb calendar
# usage: ble-histdb week

function ble/histdb/sub:calendar {
  local palette=$1

  local grboard_export
  ble/histdb/graph/board/initialize '' "$palette"
  local -x "${grboard_export[@]}" # disable=#D1566

  local grvbar_declare
  ble/histdb/graph/vbar/initialize "$palette[2]"
  local -x "${grvbar_declare[@]}" # disable=#D1566

  local awk
  if ble/is-function ble/bin/mawk; then
    awk=ble/bin/mawk
  elif ble/is-function ble/bin/gawk; then
    awk=ble/bin/mawk
  else
    printf 'ble-histdb-calendar: gawk or mawk is needed.' >&2
    return 1
  fi

  ble/histdb/sub:query -separator ' ' "SELECT strftime('%Y %j %w', issue_time, 'unixepoch', 'localtime') AS date FROM command_history;" |
    sort | uniq -c | "$awk" '
      '"$_ble_histdb_grboard_awk_lib"'
      '"$_ble_histdb_grvbar_awk_lib"'

      BEGIN {
        grboard_initialize();
        grvbar_initialize();

        split("Sun,Mon,Tue,Wed,Thu,Fri,Sat,   ", c_weekday, ",");
        split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", c_month, ",");
      }
      function initialize_year(year, _, i) {
        g_year = year;
        g_year_nday = 0 + strftime("%j", mktime(year " 12 31 01 00 00"));
        g_year_wday_offset = 0 + strftime("%w", mktime(year " 01 01 01 00 00"));
        g_score_max = 0;
        g_score_total = 0;
        for (i = -g_year_wday_offset; i < g_year_nday; i++)
          g_score[i] = 0;
      }
      function flush_year(_, line, i, x, y, w, m, iday, iweek, lines, score_table, score_weekly) {
        if (g_year == "" || g_year == 1970) return;

        print "\x1b[1m# " g_year " (total = " g_score_total ", max = " g_score_max ")\x1b[m";
        print "";

        line = c_weekday[8];
        x = 0;
        for (m = 1; m <= 12; m++) {
          iday = strftime("%j", mktime(g_year sprintf(" %02d 01 01 00 00", m))) - 1;
          iweek = int((iday + g_year_wday_offset) / 7);
          line = line sprintf("%*s", iweek * 2 - x, "") c_month[m];
          x = iweek * 2 + length(c_month[m]);
        }
        print line;

        # Fill score_table and score_weekly
        w = 0;
        for (i = -g_year_wday_offset; i < g_year_nday; i++) {
          y = (i + g_year_wday_offset) % 7;
          x = int((i + g_year_wday_offset) / 7);
          if (x >= w) w = x + 1;

          score_table[y, x] = i < 0 ? -1 : g_score[i];
          score_weekly[x] += g_score[i];
        }

        grboard_render(lines, score_table, w, 7);
        for (y = 0; y < 7; y++) print c_weekday[1 + y] lines[y];

        grvbar_render(lines, score_weekly, w, 3);
        for (y = 0; y < 3; y++) print c_weekday[8] lines[y];

        print "";
      }

      $2 != g_year {
        flush_year();
        initialize_year($2);
      }
      {
        yday = $3 - 1;
        g_score[yday] = $1;
        g_score_total += $1;
        if ($1 > g_score_max)
          g_score_max = $1;
      }

      END {
        flush_year();
      }
    '
}

function ble/histdb/sub:week {
  local palette=$1

  local grboard_export
  ble/histdb/graph/board/initialize '●' "$palette"
  local -x "${grboard_export[@]}" # disable=#D1566

  local grvbar_declare
  ble/histdb/graph/vbar/initialize "$palette[2]"
  local -x "${grvbar_declare[@]}" # disable=#D1566

  local grhbar_declare
  ble/histdb/graph/hbar/initialize "$palette[2]"
  local -x "${grhbar_declare[@]}" # disable=#D1566

  ble/histdb/sub:query -separator ' ' "SELECT strftime('%w %H', issue_time, 'unixepoch', 'localtime') AS date FROM command_history;" |
    sort | uniq -c | ble/bin/awk '
      '"$_ble_histdb_grboard_awk_lib"'
      '"$_ble_histdb_grvbar_awk_lib"'
      '"$_ble_histdb_grhbar_awk_lib"'

      BEGIN {
        grboard_initialize();
        grvbar_initialize();
        grhbar_initialize();
        split("Sun,Mon,Tue,Wed,Thu,Fri,Sat,   ", c_weekday, ",");
      }
      {
        y = $2 + 0;
        x = $3 + 0;
        g_score[y, x] += $1;
        g_score[x] += $1;
        g_score_wday[y] += $1;
      }
      END {
        line = c_weekday[8];
        for (h = 0; h < 24; h++)
          line = line sprintf(h % 3 == 0 ? "%02d" : "  ", h);
        print line;

        grboard_render(lines, g_score, 24, 7);
        grhbar_render(lines_hbar, g_score_wday, 4, 7);
        for (y = 0; y < 7; y++) print c_weekday[1 + y] lines[y] lines_hbar[y];

        grvbar_render(lines, g_score, 24, 3);
        for (y = 0; y < 3; y++) print c_weekday[8] lines[y];
      }
    '
}
