local ARGS = { ... }


if not string.find(arg[-1],'lua523r') then
   table.move(package.searchers, 1, #package.searchers, 2)
   package.searchers[1] = function(...)
      print('searching... ', ...)
      return
   end
end

require 'clibs'
-- require 'parseOpts' 
local Nylon = require 'nylon.core'()

wv = require 'nylon.debug' { name = 'pls-main' }

wv.log('debug', 'pls-main created; syscore.addCallback=%s', type(NylonSysCore.addCallback))

local Sqlite  = require 'sqlite'

-- add path for nylabus services
package.path = '../nylabus/?.lua;' .. package.path


glOpts = {
   space = 'ncr'
}

if ARGS[1] then
   glOpts.space = ARGS[1]
end


local Theme = require 'pls-theme'

local plugin = {
   fixLine = function() end,
   onEditRecord = function() end,
}
pcall( function()
   plugin = loadfile( 'space/' .. glOpts.space .. '/plugin.lua' ){
      Theme = Theme
   }
end)



local VLINE = tonumber(Pdcurses.Lines.vline)
local HLINE = string.byte('-',1) -- Some fonts don't have a good HLINE character, so use the dash.  Suboptimal, but some fonts, like Monaco, are really nice except for this deficiency.
local HLINE = tonumber(Pdcurses.Lines.hline)


local WINDOWS = (NylonOs and NylonOs.IsWindows())

wv.log('debug', 'color_pairs=%d', Pdcurses.Static.color_pairs() )


local json    = require 'JSON' -- for debugging
local Winman  = require 'pls-winman'
local Numword = require 'pls-numword'

local Service
local ok, err = pcall( function() Service = require 'nylaservice' end )
if not ok then
   wv.log('debug','could not load nylaservice, e=%s', err )
end

local gIdUser = 1 -- dmattp, default

local cord_app -- this should be moved down to creation point by removing single use in make_editor()


if arg[1] then
   recid = tonumber(arg[1])
end

local dbname = ( 'space/' .. (glOpts.space) .. '/notes.db' )

-- print('db=', dbname)

local db = Sqlite:new( dbname )
   if not db then
      wv.log('error', 'db.a01 could not open name=%s', dbname)
      error 'no database'
   else
      wv.log('debug', 'db open db=%s db.db = %s', tostring(db), type(db.db))
   end

--   db.db:testVoid();
--   db.db:testInt(123456);
--   error 'done okay'
-- db:exec('select count(*) from notes')
   
-- open up the most recently edited record to start
local lastnote = db:selectOne('select ROWID from note where dt_modified=(select MAX(dt_modified) from note)')

if not lastnote then
   local rc = db:exec('insert into note (detail,title,dt_created,dt_modified,id_user) values (?,?,DATETIME("NOW"),DATETIME("NOW"),?)',
                       'Welcome to PLS Notes', 'Welcome - First Record', gIdUser)
   lastnote = db:selectOne('select ROWID from note where dt_modified=(select MAX(dt_modified) from note)')
end
local recid = lastnote.ROWID


local Buffer = require 'pls-buffer'

local keyhandler


local curses_started

local function start_curses()
   if curses_started then
      Pdcurses.Static.refresh()
   else
      Pdcurses.Static.noecho()
      Pdcurses.Static.start_color()
      Pdcurses.Static.raw()

      -- when mouse enabled, generates Pdcurses.key.mouse
      -- Pdcurses.Static.mouse_on(0x1fffffff) -- ALL_MOUSE_EVENTS
      curses_started = true
   end       
end

local function end_curses()
   if curses_started then -- and (not Pdcurses.Static.isendwin()) then
      wv.log('debug', 'stopping curses end_curses()')
      Pdcurses.Static.endwin()
   end
end

local function curses_init()
   local screen = Pdcurses.Static.initscr()
   start_curses()
   local ncols, nrows = screen:getmaxx(), screen:getmaxy()
   return { screen = screen, ncols = ncols, nrows = nrows }
end


local env = { curses = curses_init() }


local function shell_cmd( fmt, ... )
   Pdcurses.Static.endwin()
   --  -noprofile maybe nice, or not.
   os.execute( string.format('powershell -noninteractive /c "%s"'
                  ,string.format(fmt,...) ) )
   Pdcurses.Static.noecho()
   env.curses.screen:redraw()
   env.curses.screen:refresh()
   Winman.Refresh()
end


local function simple_shell_cmd( fmt, ... )
   Pdcurses.Static.endwin()
   --  -noprofile maybe nice, or not.
   os.execute( string.format(fmt,...) )
   Pdcurses.Static.noecho()
   env.curses.screen:redraw()
   env.curses.screen:refresh()
   Winman.Refresh()
end


local function WindowDim( x, y, w, h )
   return { x = x, y = y, w = w, h = h }
end


function centerwindow(env,h,w)
   local x = (env.curses.ncols - w)/2
   local y = (env.curses.nrows - h)/2
   return h, w, y, x
end


local function withFocusAttrib( mw, fun )
   Theme.with.focuswinborder( Winman.isFocused(mw) and mw.win, fun )
end


function ui_yesno(cord,env,prompt,default)
   local wid = (#prompt)+2
   wid = wid < 14  and 14 or wid
   local w = Pdcurses.Window(centerwindow(env,4,wid))


   local function showopt( hiliteyes )
      Theme.with.inverse( hiliteyes and w, function()
                             w:mvaddstr(2,2,'[Y]es')
      end)
      Theme.with.inverse( (not hiliteyes) and w, function()
                             w:mvaddstr(2,8,'[N]o')
      end )
      w:refresh()
   end
   
   default = default ~= nil and default or true

   local mw

   local function drawme()
      withFocusAttrib( mw, function() w:stdbox_() end )
      w:mvaddstr(1,1,prompt)
      showopt(default)
   end

   local removeme
   local yes = Nylon.self:sleep_manual(
      function(wakefun)
         mw = Winman.win:new{ win = w, on = { key = function(k)
                                                       local s = (type(k) == 'number' and k < 256 and string.char(k))
                                                       wv.log('debug','yesno got key=%s/%s',k,s)
                                                       if s == 'n' or s == 'N' then
                                                          wakefun(false)
                                                       elseif s == 'y' or s == 'Y' then
                                                          wakefun(true)
                                                       elseif k == 27 then -- escape
                                                          wakefun(false)
                                                       elseif default and k == 10 then -- newline
                                                          wakefun(default)
                                                       end
                                  end,
                                  resized = function()
                                     drawme()
                                  end} }
         drawme()
         removeme = Winman.push_modal(mw)
      end)

   -- flash yes/no to provide feedback
   showopt( yes )
   w:refresh()
   Nylon.self:sleep(0.15)
   removeme()

   return yes
end


function ui_oneline(env,prompt,opt)
   opt = opt or {}
   local cord = Nylon.self
   local wid = type(opt)=='table' and opt.w or math.floor(env.curses.ncols * 0.8)


   local w = Pdcurses.Window(centerwindow(env,3,wid))
   local function draw()
      Theme.with.inverse( w, function()
         w:box( VLINE, HLINE )
         w:mvaddstr(0, 3, '[ ' .. prompt .. ': ]')
         w:mvaddstr(1, 1, string.rep(' ', wid-2))
         -- w:mvaddstr(2, 2, ' [E]nter ')
      end)
      w:refresh()
   end

   local ed = require 'pls-ed'
   local bbuffer = Buffer.withText( (type(opt)=='string') and opt or opt.text or '')

   local e = setmetatable( { w = w,
                             on = {}
                           }, { __index = env } )

   local editcord = Nylon.cord( 'editoneline', ed.entryfn_edit, e, bbuffer, 
                                { wdim = WindowDim(1,1,wid-2,1),
                                  theme = { text = 'inverse' },
                                  oneLine = true } )

   if opt and opt.text and opt.replace then
      if true then
         editcord.event.move_end_of_line()
         Nylon.self:sleep(0.03)
         editcord.event.set_mark_command()
         Nylon.self:sleep(0.03)
         editcord.event.move_beginning_of_line()
      end
   end

   local mw = Winman.win:new{ win = w, on = { key = function(k)
                                                 editcord.event.key(k)
                            end,
                                              resized = function()
                                                 draw()
                                                 editcord.event.redraw()
                            end} }

   local removeme = Winman.push_modal( mw )
   
   draw()
                                
   local edited = Nylon.self:sleep_manual(function(wakefun)
                              function e.on.save( text )
                                 wv.log('debug','got save oneline text=%s',text)
                                 wakefun( text )
                              end
                              function e.on.cancelled()
                                 wv.log('debug','cancelled oneline text edit')
                                 wakefun()
                              end
   end)
   
   removeme()

   -- @todo: shutdown should be checked by the cord to break out of loop!
   editcord.event._shutdown()

   wv.log('debug','got edited text=%s',edited)

   return edited
end





-- global, meh
--- function request_keys( cbfun )
---    local old = keyhandler
---    local enabled = true
---    keyhandler = function(k)
---       local handled = (enabled and cbfun(k)) or (old and old(k))
---       wv.log('norm','keyhandled? %d=%s', k, handled and 'yes' or 'no')
---    end
---    return function()
---       enabled = false
---    end
--- end






local function picklist( title, query_sql, ... )

   local on_callbacks = {}
   if type(title) == 'table' then
      if title.on then
         on_callbacks = title.on
      end
      title = title.title
   end
   
   local records = {}
   
   local function run_db_query(...)
      local ok, err = pcall( function(...)
               records = db:selectMany( query_sql, ... )
      end, ...)
      if not ok then
         wv.log('error','error running SQL query=%s', tostring(err))
      end
   end

   run_db_query(...)

   local width = math.floor(env.curses.ncols/3)
   local height = math.floor(env.curses.nrows*2/3)
   if height > #records + 2 then
      height = #records + 2
   end
   local dim = WindowDim(env.curses.ncols-width,0,width,height)
   local w = Pdcurses.Window(dim.h, dim.w, dim.y, dim.x)

--   wv.log('debug','tostring=%s type=%s', tostring, type)
--   wv.log('debug','created WINDOW w=%s', type(w))
--   wv.log('debug','created WINDOW w.mvaddstr=%s,%s',tostring(w.mvaddstr),type(w.mvaddstr))
--   wv.log('debug','created WINDOW w.box=%s,%s',tostring(w.box),type(w.box))

   local active = #records > 0 and 1 or 0
   local theTop = 0

   local mw 


   local sstring

   local function draw()
      if (dim.w < 2) or (dim.h <2) then
         return
      end
      local maxtextlen = dim.w - 2
      withFocusAttrib( mw, function()

--                              wv.log('debug','draw WINDOW w=%s,%s',tostring(w),type(w))
--                              wv.log('debug','draw WINDOW w.mvaddstr=%s,%s',tostring(w.mvaddstr),type(w.mvaddstr))
--                              wv.log('debug','draw WINDOW w.box=%s,%s',tostring(w.box),type(w.box))

                          w:box(VLINE, HLINE)
                   w:mvaddstr(0,3,title)
      end )
      for row = 1, dim.h-2 do
         local drawingRecord = row + theTop
         local r = records[drawingRecord]
         if not r then break end
         local text = string.format("%.10s %5s  %s", r.dt_modified, Numword.to_s(r.ROWID),
                                        r.title):sub(1,maxtextlen)

         Theme.with.inverse( drawingRecord == active and w, function()
                      local s,e
                      if sstring then 
                         s,e = text:lower():find(sstring) 
                      end
                      if s then
                         w:mvaddstr(row,1,text:sub(1,s-1))
                         Theme.with.classicmenu( w, function()
                                     w:addstr(text:sub(s,e))
                         end)
                         Theme.with.inverse( drawingRecord == active and w, function()
                                               w:addstr(text:sub(e+1))
                         end)
                      else
                         w:mvaddstr(row,1,text)
                      end
         end )

         if #text < maxtextlen then
            w:addstr( string.rep(' ',maxtextlen-#text) )
         end
      end
      if sstring then
         Theme.with.classicmenu( w, function()
                                    w:mvaddstr(dim.h-1,4,'[i-search: ' .. sstring .. ' ]')
         end)
      end
      w:refresh()
   end
   
   local removewin

   local cord = Nylon.cord('picklist',
      function(cord,args)
         function cord.event.beginning_of_buffer()
            theTop = 0
            active = 1
            draw()
         end
         function cord.event.end_of_buffer()
            active = #records
            theTop = active - (dim.h-2)
            draw()
         end
         function cord.event.scroll_up_command()
            active = active + (dim.h-2)
            if active > #records then
               active = #records
            end
            if active > theTop + (dim.h-2) then
               theTop = active - (dim.h-2)
            end
            draw()
         end
         function cord.event.next_line()
            if active < #records then
               active = active + 1
               if active > theTop + (dim.h-2) then
                  theTop = active - (dim.h-2)
               end
               draw()
            end
         end
         function cord.event.scroll_down_command()
            active = active - (dim.h-2)
            if active < 1 then
               active = 1
            end
            if (active-1) < theTop then
               theTop = (active-1)
            end
            draw()
         end
         function cord.event.previous_line()
            if active > 1 then
               active = active - 1
               if (active-1) < theTop then
                  theTop = (active-1)
               end
               draw()
            end
         end

         local done
         function cord.event.kill_buffer()
            done = true
            removewin()
            wv.log('debug','close picklist, on_callbacks.closed=%s',on_callbacks.closed)
            local _ = on_callbacks.closed and on_callbacks.closed()
         end
         
         -- very simple and dumb refresh mechanism
         while true do
            cord:sleep(20)

            if done then break end

--            wv.log('debug','run db query #records=%d #args=%d',#records,#args)
            run_db_query(table.unpack(args))
--            wv.log('debug','ran db query, #records=%d',#records)
         -- this is a minor problem that if a modal window is up,
         -- redrawing will clobber the modal window
            draw()
         end
      end, { ... } )


   local function isearch_search_from(sstring,from,dir)
      dir = dir or 1
      local nrecords = #records
      if nrecords < 1 then wv.log('error','no search records?'); return end
      for j = from, from+(nrecords*dir), dir do
--         wv.log('debug','isrch from=%d dir=%d j=%d #records=%d', from, dir, j, #records )
         local i = ((j-1) % nrecords)+1
--         wv.log('debug','isrch from=%d j=%d i=%d dir=%d', from, j, i, dir )
         if string.find(records[i].title:lower(),sstring) or 
            string.find(Numword.to_s(records[i].ROWID):lower(),sstring) then
            active = i
            return
         end
      end
--      ding()
   end

   local function isearch_find_next(str,dir)
      dir = dir or 1
      isearch_search_from(sstring,active+dir,dir)
      draw()
   end

   local function isearch_on_string_update(sstring)
      isearch_search_from(sstring,active)
      draw()
   end

   mw = Winman.win:new{ win = w, on = { 
                                 resized = function(newdim) 
                                    dim = (newdim or dim)
                                    wv.log('debug','picklist resized, %dx%d@%d,%d', dim.w, dim.h, dim.x, dim.y)
                                    draw()
                                 end,
                                 key = function(k)
                                    wv.log('debug','picklist got key=%s',k)
                                    if type(k) == 'string' and cord:has_event(k) then
                                       cord.event[k]()
                                    elseif k == 10 then -- newline
                                       local _ = records and records[active] and cord_app.event.OpenRecord( records[active].ROWID )
                                    elseif k == 46 then -- '.'
                                       if not sstring then
                                          cord.event.kill_buffer()
                                          local _ = records and records[active] and cord_app.event.OpenRecord( records[active].ROWID )
                                       else
                                          sstring = sstring .. string.char(k)
                                          isearch_on_string_update( sstring )
                                       end
                                    elseif k == 8 and sstring then
                                       sstring = sstring:sub(1,#sstring-1)
                                       isearch_on_string_update(sstring)
                                    elseif k == 19 or k == 18 then -- C-s
                                       if sstring then
                                          isearch_find_next(sstring, (k == 19) and 1 or -1)-- go to next after current
                                       else
                                          sstring = '' -- start isearch forward
                                          draw()
                                       end
                                    else
                                       if sstring then
                                          if type(k) == 'number' and k >= 32 and k <= 128 then
                                             sstring = sstring .. string.char(k)
                                             isearch_on_string_update(sstring)
                                          elseif k == 27 or k == 7 then -- ESC or Ctrl+g
                                             sstring = nil
                                             draw()
                                          end
                                       else
                                          return k
                                       end
                                    end
                                 end } }
   removewin = Winman.push( mw )
end

local function args_concat( ... )
   local t = { ... }
   return table.concat(t)
end


local function menuOptions( keys )
   menutext = {}
   for _,v in pairs(keys) do
      local key = type(v)=='string' and v:sub(1,1) or v[1]
      local str = type(v)=='string' and v:sub(2)   or v[2]
      table.insert( menutext, args_concat( '[', key, ']', str ) )
   end
   return menutext
end

local function MakeMenu( keys )
   local menuText = menuOptions(keys)
   local text = args_concat( ' ', table.concat(menutext, ' '), ' ' )
   
   local menuwin = Pdcurses.Window(1,#text,0,0)
   local function draw()
      Theme.with.classicmenu( menuwin, function()
                                 menuwin:mvaddstr(0,0,text)
      end)
      menuwin:refresh()
   end
   return menuwin, draw
end

local function MakeMenuVert( keys )
   local menuText = menuOptions(keys)
   local maxlen = 0
   for i, v in ipairs(menuText) do
      v = args_concat(' ', v, ' ')
      menuText[i] = v
      maxlen = (#v > maxlen) and #v or maxlen
   end

   wv.log('debug','MakeMenuVert: maxlen=%d #menuText=%d', maxlen, #menuText)

   local text = args_concat( ' ', table.concat(menutext, ' '), ' ' )
   
--   local menuwin = Pdcurses.Window(#menuText,maxlen,0,0)
   local menuwin = Pdcurses.Window(#menuText,maxlen,1,0)
   local function draw()
      Theme.with.classicmenu( menuwin, function()
                                 for i, v in ipairs(menuText) do
--                                    menuwin:mvaddstr( 1+i, 0, v )
                                 menuwin:mvaddstr(i-1,0,v)
                                 menuwin:mvaddstr(i-1,#v, ('           '):sub(1,maxlen-#v))
                                 end
      end)
      menuwin:refresh()
   end
   return menuwin, draw
end


   local function menuedFunctions( invokingCord, ftab, opt )
      local tOptions = {}
      for k, _ in pairs(ftab) do
         table.insert( tOptions, k )
      end
      local menuwin
      local drawmenu
      if opt and opt.vert then
         menuwin, drawmenu = MakeMenuVert(tOptions)
      else
         menuwin, drawmenu = MakeMenu(tOptions)
      end
      local grabbed = {}
      for k, v in pairs(ftab) do
         local optkey
         if type(k) == 'table' then
            optkey = string.byte(k[1],1)
         else
            optkey = string.byte(string.lower(string.sub(k,1,1)),1)
         end
         grabbed[ optkey ] = v
      end
      local managed = Winman.win:new{ win = menuwin,
                                  on = { key = function(k) invokingCord.event.exitContextMenu(k) end,
                                         resized = function() drawmenu() end
      } }
      local winman_remove = Winman.push_modal( managed )
      local key = invokingCord:sleep_manual( function(wakefun)
                                           invokingCord.event.exitContextMenu = function(k)
                                              wv.log('debug','app key grab, k=%s',k)
                                              wakefun(k)
                                           end
      end )
      winman_remove()
      if grabbed[key] then
         wv.log('debug','app grab key 2=%s',key)
         grabbed[key]()
      end
   end



local function getrecid(recid)
   recid = recid and (type(recid)=='number' or recid:find('^%d')) and recid or Numword.to_i(recid)
   return recid
end
   
local function FindRecordById(recid1)
   local altrecid
   if type(recid1) == 'table' then
      altrecid = recid1[2]
      recid1 = recid1[1]
      wv.log('debug','got rec/alt=%s/%s',recid1, altrecid)
   end
   local recid = getrecid(recid1)
   wv.log('debug','getrecid=%s recid1=%s',recid, recid1)

   if not recid and altrecid then
      recid = getrecid(altrecid)
   end

   if not recid then
      if not recid1 then
         recid = ui_oneline(env,'Enter record id', {w=30})
         if not recid then -- cancelled
            return
         end
         recid1 = recid
         local r2 = getrecid(recid)
         wv.log('debug','getrecid oneline=%s r2=%s',recid, r2)
         recid = r2
      end
   end

   local function selectBestMatch(set,id)
      id = id:lower()
      local strmatch = ':' .. id .. '%W'
      local best
      for _, r in pairs(set) do
         local downtitle = r.title:lower()
         local hasExact = string.find( downtitle, ':' .. id .. '$')
         wv.log('debug','selectBestMatch match=/%s/ hasExact=%s title="%s" r=%s best=%s', strmatch, hasExact, r.title, r, best)
         if hasExact then -- always prefer a record whose title is exactly the tag!
            best = r
         else
            best = best or (string.find( downtitle, strmatch ) and r) -- secondly prefer a record which has the :tag followed by non-word (space,punctuation,etc; not 's' for plural!)
         end
      end
      return best or set[1]
   end

   local record
   if recid then -- check for keyword
      record = db:selectOne('select rowid, title, detail, dt_created, dt_modified from note where rowid=?', tonumber(recid) )
   end
   if record then
      return record, false, recid1
   else
      wv.log('debug','looking for title like=%s',recid1)

      -- prefer records with a leading ":" as a tag, with the exact string first followed by a prefix/stem match
      records = db:selectMany('select rowid, title, detail, dt_created, dt_modified from note where title like ?', '%:' .. recid1 .. '%' )
      record = selectBestMatch( records, recid1 )
      record = record or db:selectOne('select rowid, title, detail, dt_created, dt_modified from note where title like ?', '%' .. recid1 .. '%' )

      return record, true, recid1
   end
end

local Keys = require 'pls-keys'


local on_save_scan_citations = require 'on-save-scan-citations'


local gOpenRecords = {}


local function make_editor( env, record )
   local ed = require 'pls-ed'

   if record.ROWID and gOpenRecords[record.ROWID] then
      wv.log('abnorm','record[%s] already open', Numword.to_s(record.ROWID))
      local win = gOpenRecords[record.ROWID]
      Winman.this_window_to_primary(win)
      return
   end

   
   local nrows, ncols = env.curses.nrows, env.curses.ncols
   local dim = WindowDim( 0, 0, math.floor(ncols/2), env.curses.nrows )

   local currentRev = 0
   local function setCurrentRev()
      if record.ROWID then
         local found = db:selectOne('SELECT COUNT(*) as rev from patch where id_note=?',record.ROWID)
         currentRev = found and tonumber(found.rev) or currentRev
      end
   end
   setCurrentRev()
   
   ------------------------------------------------------------------
   ---- Border
--   local wb = Pdcurses.Window(h+2,w+2,y-1,x-1)
--   wb:stdbox_()
--   wb:refresh()


   ------------------------------------------------------------------
   ---- Edit window
   local win = Pdcurses.Window( dim.h, dim.w, dim.y, dim.x )
   local mw = { win = win } -- Winman managed window, set at the bottom of this function


   local function drawbox()
      withFocusAttrib( mw, function() 
                          -- win:stdbox_()
                          win:box( tonumber(VLINE), HLINE ) --5VLINE, '-' ) 
      end )
   end

   local bbuffer

   ------------------------------------------------------------------
   ---- Title
   local function settitle()
      local recword
      if record.ROWID then
         recword = Numword.to_s(record.ROWID)
      else
         recword = "*NEW*"
      end
      local ttext = ' ' .. record.title:sub(1,dim.w-16) .. ' [ ' .. 
         recword .. '.' .. currentRev .. (bbuffer and bbuffer:isModified() and '* ] ' or ' ] ')
      win:move(0,1)
      withFocusAttrib( mw, function()
         win:hline(HLINE,dim.w-2)
         win:mvaddstr(0,(dim.w-2-#ttext),ttext)
      end )
   end

   settitle()

   local cord = Nylon.self

   local function editTitle()
      wv.log('debug','got call to editTitle')
      local starttext = record.title == 'new record' and '' or record.title
      local title = ui_oneline(env,'Edit Title', { text=starttext })
      if title then
         record.title = title
         settitle()
         win:refresh()
         if record.ROWID then
            db:retryexec('update note set title=?,dt_modified=DATETIME("NOW") where rowid=?', title, record.ROWID)
         end
      end
      return true
   end

   ------------------------------------------------------------------
   ---- Status bar
   -- local statuswin = Pdcurses.Window(1,w,y+h+1,x)
   local laststatus
   local function winstatus( sttype, data )
      -- statuswin:hline(HLINE,w+2)
      local prevx, prevy = win:getx(), win:gety()
      local statusrow = dim.y + dim.h - 1
      laststatus = data
      local crdate = '???'
      local update = '???'
      if record and record.dt_created then
         local d = record.dt_created
         crdate = d:sub(3,4) .. d:sub(6,7) .. d:sub(9,10)
      end
      if record and record.dt_modified then
         local d = record.dt_modified
         update = d:sub(3,4) .. d:sub(6,7) .. d:sub(9,10)
      end
      data = string.format('date=%s/%s %s', crdate, update, data)
      win:move(statusrow,1)
      withFocusAttrib( mw, function()
                          win:hline(HLINE,dim.w-2)
      end )
      -- statuswin:clear()
      -- statuswin:move(0,0)
      win:mvaddstr(statusrow,3,'[')
      win:addstr(data)
      win:addstr(']')
      --local left = w - 6 - #data + 2
      --statuswin:addstr(string.rep('_',left))
      win:move(prevy, prevx)
      win:refresh()
   end

   local titlecache = {}


   local editcordrefresh = nil
   
   local function plsed_drawline( win, x, y, text, markBeg, markEnd )

      if #text > (dim.w - 2) then
         win:mvaddstr( y, x, text:sub(1,dim.w-2) )
         return dim.w-2
      end

      -- markBeg, markEnd indicate the text in the section should
      -- be highlighted
      if not markBeg then
         local b,e,hdr,htext = text:find("^(h%d%.)(.*)") -- textile specific highlighting!! DANGER!! DANGER!!
         if not b then
            win:mvaddstr( y, x, text )
         else
            win:mvaddstr( y, x, hdr )
            Theme.with.heading( win, function()
                                   win:addstr(htext)
            end)
         end
      else
         local endPoint = markEnd > (dim.w-2) and (dim.w-2) or markEnd
         win:move(y,x)
         win:addstr( text:sub(1,markBeg-1) )
         Theme.with.inverse( win, function()
                                win:addstr( text:sub(markBeg,endPoint) )
         end )
         win:addstr( text:sub(endPoint+1,dim.w-2) )
      end

      local drawn = #text
      local remain = (dim.w -2) - drawn

      local s,e,tag = string.find(text,':(%w+)$')
      if s then
         if Numword.isvalid( tag ) then
         -- win:move(y,x)
            if not titlecache[tag] then
               local rec = db:selectOne( 'select title from note where rowid=?',
                                         Numword.to_i(tag) )
               titlecache[tag] = rec and rec.title
            end

            local append = titlecache[tag] or '???'
            local todraw = string.sub( (' ' .. append), 1, remain)
            Theme.with.autotext( win, function()
                                    win:addstr( todraw ) -- text.sub(s, e) )
            end)
            return drawn + #todraw
         end
      end

      -- here we look for codings, e.g., cross-system references that may be
      -- unique to the active space
      plugin.fixLine(text, win)
   end

   local e = setmetatable( { w = win,
                             report = winstatus,
                             on = {}
                           }, { __index = env } )

   -- for simple testing
   bbuffer = Buffer.withText( record.detail )

   local editordim = WindowDim(1,1,dim.w-2,dim.h-2)

   local cord_edit  = Nylon.cord( 'edit', ed.entryfn_edit, e, bbuffer,
                             { wdim = editordim,
                               plug = {
                                  drawline = plsed_drawline
                               }
   } )

   editcordrefresh = function() cord_edit.event.refresh() end
   
   local removewin -- callback set when Winman.push is called

   -- kill_buffer() callback from editor cord
   function e.on.kill_buffer()
      -- cord.event._shutdown()
      wv.log('debug','got kill buffer removewin=%s',removewin)
      if record.ROWID then
         gOpenRecords[record.ROWID] = nil
      end
      removewin()
   end

   
   
   function e.on.contextmenu(wordAtPoint, opt)
      wv.log 'got on.contextmenu callback'
      local function show_insert_menu()
         local function insert_link()
            local record, makenew, recid1 = FindRecordById(wordAtPoint)
            wv.log('debug', 'frbid record=%s makenew=%s recid1=%s', record, makenew, recid1 )
            if record then
               local id = record.ROWID
               local nw = Numword.to_s(id)
               wv.log( 'debug', 'got record, id=%s / nw=%s', id, nw )
               local text = string.format(':%s\n', nw)
               local _ = opt and opt.consume_word and opt.consume_word()
               cord_edit.event.insert_at_point( text )
            end
         end
         menuedFunctions( cord_edit, {
                             XML = function() end,
                             Nonsense = function() end,
                             Link = insert_link
                                     }, { vert = true })
      end

      -- @todo: check buffer for newlines
      local function insert_yank_with_tag( tag )
         -- if buffer:char_at_point(thePoint) ~= '\n' then cord_edit.event.insert_at_point( '\n' ) end
         cord_edit.event.insert_at_point( '\n<' .. tag .. '>\n' )
         cord_edit.event.yank()
         Nylon.self:sleep(0.01) -- give yank process time to complete; hate this, but yank i think depends on events propogating back to main cord
         -- if buffer:char_at_point(thePoint) ~= '\n' then cord_edit.event.insert_at_point( '\n' ) end
         cord_edit.event.insert_at_point( '</' .. tag .. '>\n' )
      end

      
      menuedFunctions( cord_edit,
                       {
                          Insert = show_insert_menu,
                          Paste = function()
                             menuedFunctions( cord_edit, {
                                                 Pre = function() insert_yank_with_tag 'pre' end,
                                                 Quote = function() insert_yank_with_tag 'blockquote' end
                                                         }, { vert = true })
                          end
      })
   end

   local function possibly_transform_clipboard_text( text )
      wv.log('debug', 'transform clibpboard=[[%s]]', text)
      if string.find(text,'^(http://.*)') or string.find(text,'^(https://.*)') then
         return string.gsub(text, ' ', '%%20')
      else
         return text
      end
   end
   
   
   function e.on.yank(special)
      if not NylonOs then
	return
      end
      local puthtm
      --      wv.log('debug','got yank, special=%s',special)
      local cbrc = {}
      NylonOs.Static.getclipboard_ext(
         function(tyyp, content)
            cbrc = { tyyp = tyyp, content = content }
      end )
      
      if cbrc.tyyp then
         local tyyp = cbrc.tyyp
         local content = cbrc.content
         
--            wv.log('debug','clipboard has data of type=%s',tyyp)

         -- this is sort of unique to windows; see internet docs on on CF_HTML  
         -- clipboard content type.
         if special and tyyp == 'html' then
            local _, _, m = content:find '<!%-%-StartFragment%-%->(.*)<!%-%-EndFragment%-%->'
            if m then
               cord_edit.event.insert_at_point( m ) -- :sub(s,e)
               puthtm = true
            end
         end

         if tyyp == 'text' and (not puthtm) then
            cord_edit.event.insert_at_point( possibly_transform_clipboard_text(content) )
         end


         if tyyp == 'CF_HDROP' then
            (function()
               local File = require 'filelib'
               local Table = require 'extable'
               local fileURLsRaw = Service.archiver.archiveFileList( content )
               local fileURLs = Table.map( function(url)
                                         local base = File.leaf(url)
                                         return string.format('"%s":%s', base, url)
                                       end, fileURLsRaw )
               local itext
               if #content > 1 then
                  itext = '* ' .. table.concat( fileURLs, '\n* ' )
               else
                  itext = fileURLs[1]
               end
               cord_edit.event.insert_at_point( itext )
            end)()
         end

         if tyyp == 'image/png' then
            dbimg = dbimg or (Sqlite:new 'images.db')
            dbimg:exec('insert into image (raw) values (?)', content)
            local rowid = dbimg:lastRowId()
            cord_edit.event.insert_at_point( 
               string.format("!:img:%s!\n", Numword.to_s(rowid) ) )
            local copy = dbimg:selectOne('select raw from image where ROWID=?', rowid);
            if copy then
               wv.log('debug','Got image out, sz=%d', #copy.raw)
            else
               wv.log('error','cant read saved image??')
            end
            -- shell_cmd 'get-process snippingtool | stop-process"'
         end
      end

      return true;
   end

   -- save() callback from editor cord
   function e.on.save( text )
      wv.log('debug','save new record=%d text=%s',record.ROWID,text:sub(1,40))

      local prevText = (currentRev > 0) and record.detail or ''

      local Diff = require 'diff_match_patch'
      -- create patch to turn text into previous text
      local patches = Diff.patch_make(text,prevText)
      local patchtext = Diff.patch_toText(patches)
      -- create patch to turn previous text into text
      -- hate doing seperate fwd/reverse patchtext, but there's not an easy algo provided with diffmatchpatch for reversing
      local rpatches = Diff.patch_make(prevText,text)
      local rpatchtext = Diff.patch_toText(rpatches)
      if #patchtext > 0 then
         wv.log('debug','edit, patch is this (retryexec=%s):\n%s', tostring(db.retryexec), patchtext)
         db:retryexec('insert into patch (id_note,dt_created,content,rcontent,id_user,revision) values (?,DATETIME("NOW"),?,?,?,(SELECT COUNT(*) from patch where id_note=?))',
                      record.ROWID, patchtext, rpatchtext,gIdUser, record.ROWID)
      end

      on_save_scan_citations( db, record.ROWID, text )

      local isNew = false
      if record.ROWID then
         db:retryexec('update note set detail=?,dt_modified=DATETIME("NOW") where rowid=?', text, record.ROWID )
      else
         local rc = db:retryexec('insert into note (detail,title,dt_created,dt_modified,id_user) values (?,?,DATETIME("NOW"),DATETIME("NOW"),?)', 
                            text, record.title, gIdUser)
         record.ROWID = db:lastRowId()
         gOpenRecords[record.ROWID] = mw
         isNew = true
      end

      plugin.onEditRecord( isNew, record )

      bbuffer:setUnmodified()
      record.detail = text
      setCurrentRev()
      settitle()
   end -- end e.on.save()

   function e.on.modified()
      settitle() -- reset the "modified" indicator
   end

   function e.on.tagActivated( tag, alttag )
      wv.log('debug','tagActivated, tag=%s (alt=%s)',tag, alttag)
      cord_app.event.OpenRecord{ tag, alttag }
   end

   function e.on.queryCitations( tag, alttag )
      local nw = Numword.to_s(record.ROWID)
      wv.log('debug','queryCitations, id=%s/%s', record.ROWID,nw)
      picklist( ('Citations of :' .. nw .. ' ' .. record.title), 
                'select ROWID,dt_modified,title from note where detail like ? order by dt_modified desc limit 50', ("%:" .. nw .. "%") )
   end

   -- callback when winman passes a key to me
   local function winman_keyhandler(k)
      if k == Keys.control.editTitle then
         cord_app:add_pending(editTitle)
      else
         if type(k) == 'string' then
            if cord_edit:has_event(k) then
               cord_edit.event[k]()
            else
               if k == 'mail_record' then
                  local recname = Numword.to_s(record.ROWID)
                  wv.log('debug','mailing record=%s',recname)
                  shell_cmd( 'mpnote-sendmail %s', recname )
--               elseif k == 'start_snipping_tool' then
--                  simple_shell_cmd 'c:\\windows\\system32\\SnippingTool.exe'
               elseif k == 'extract_to_new_record' then
                  Nylon.cord('extract_to_new_record',
                     function(cord)
                        cord:sleep_manual(
                           function(wake)
                              cord_edit.event.replace_marked( 
                                 function(marked_text)
                                    if not marked_text then
                                       wake()
                                       return
                                    end
                                    local proposedTitle = 'New Record from: ' .. record.title
                                    local newtitle = ui_oneline( env, 'Extracted Record Title', { text = proposedTitle })
                                    if not newtitle then
                                       return false
                                    end
                                    local r = { title = newtitle, detail = marked_text }
                                    local rc = db:retryexec('insert into note (detail,title,dt_created,dt_modified) values (?,?,DATETIME("NOW"),DATETIME("NOW"))', r.detail, r.title)
                                    r.ROWID = db:lastRowId()
                                    -- r.ROWID = 1379
                                    make_editor(env,r)
                                    wake()
                                    return ':' .. Numword.to_s(r.ROWID) .. '\n'
                                 end)
                           end )
                     end )
               elseif k == 'insert_ref_to_new_record' then
                  Nylon.cord(k, function(cord)
                        local function doit(wakefun)
                           local proposedTitle = 'New Record from: ' .. record.title
                           local newtitle = ui_oneline( env, 'New Record Title', { text = proposedTitle })
                           if not newtitle then
                              wakefun()
                              return
                           end
                           local r = { title = newtitle, detail = '\n\n' }
                           local rc = db:retryexec('insert into note (detail,title,dt_created,dt_modified) values (?,?,DATETIME("NOW"),DATETIME("NOW"))', r.detail, r.title)
                           r.ROWID = db:lastRowId()
                           -- r.ROWID = 1379
                           make_editor(env,r)
                           cord_edit.event.insert_at_point( ':' .. Numword.to_s(r.ROWID) )
                        end
                        cord:sleep_manual( doit )
                    end )
               elseif k == 'insert_file' then
                  Nylon.cord('insert_file_cord',function(cord)
                                cord:sleep_manual( function(wake)
                                                      local fname = ui_oneline( env,  'Insert file' )
                                                      if fname then
                                                         wv.log('debug','insert_file=%s',fname)
                                                         cord_edit.event.getPoint(function(thePoint)
                                                                                     wv.log('debug','insert_file point=%d',thePoint)
                                                                                     bbuffer:insertFileAtPoint( thePoint, fname )
                                                                                     cord_edit.event.refresh()
                                                                                     wake()
                                                         end)
                                                      end
                                end )
                  end )
               else
                  return k
               end
            end
         else
            cord_edit.event.key(k)
         end
      end
   end

   -- callback when winman resizes me
   function winman_resized( newdim )
      if newdim then
         dim.x, dim.y, dim.w, dim.h = newdim.x, newdim.y, newdim.w, newdim.h
      end
      -- editor x,y is always offset 1,1 of the window
      editordim.w, editordim.h = dim.w-2, dim.h-2
      drawbox()
      settitle()
      winstatus('resized', laststatus or '-resized-')
      wv.log('debug','winman_resized win@%d,%d , ed, %dx%d@%d,%d', dim.x,dim.y,editordim.w, editordim.h, editordim.x, editordim.y)
      if newdim then -- don't redraw if we're just changing focus
         Nylon.self:sleep_manual(function(wakefun)
                                    cord_edit.event.redraw( wakefun )
         end)
      end
      wv.log('debug','winman_resize done')
   end


   mw = Winman.win:new{ 
            win    = win,
            on = { resized = winman_resized,
                   key =     winman_keyhandler,
            } 
   }
   removewin = Winman.push( mw )

   if (record.ROWID) then
      gOpenRecords[record.ROWID] = mw
   end
   
end -- function make_editor()








local function entryfn_app( cord, env )

   cord.event.quit = function()
      local yes = ui_yesno(cord,env,'Exit now?')
      wv.log('debug','exit=%d',yes and 1 or 0)
      if yes then
         collectgarbage();
         Pdcurses.Static.clear()
         Pdcurses.Static.refresh()
         Pdcurses.Static.endwin()
         os.exit(1)
      end
      -- dmp this was hear to hide window when i was less familiar with ncurses
      -- window refreshes and before we had a window manager. it may not be needed
      -- any longer
      collectgarbage()
      Pdcurses.Static.refresh()
   end


   function cord.event.save_buffers_kill_terminal()
      cord.event.quit()
   end

   function cord.event.other_window()
      Winman.other_window()
   end

   function cord.event.focused_window_to_primary()
      Winman.focused_window_to_primary()
   end

   cord.event.SwapBuffers = function()
      wv.log 'app cord.event.SwapBuffers'
      Winman.swap_tiled()
   end

   cord.event.NewRecord = function( opt )
      local record = { title = opt and opt.title or 'new record', detail = '\n\n' }
      make_editor( env, record )
   end


   cord.event.OpenRecord = function(recid1)

      local record, makenew, recid1 = FindRecordById(recid1)

      wv.log('debug', 'frbid record=%s makenew=%s recid1=%s', record, makenew, recid1 )

      if (not record) and makenew and recid1 then
         local newt = tostring(recid1)
         if newt:sub(1,1) == ':' then
            newt = newt:sub(1,1) .. newt:sub(2,2):upper() .. newt:sub(3)
         else
            newt = ':' .. newt:sub(1,1):upper() .. newt:sub(2)
         end
         cord.event.NewRecord{ title = (newt .. ' new record') }
         return
      end
      
      if record then
         wv.log('debug','got record=%s',json:encode(record))
         make_editor( env, record )
         -- warn / status?? 
         -- 'could not find record for id=%s', tostring(recid)
      end
   end

   local lastsearch = ''

   cord.event.Search = function()
      local sstr = ui_oneline( env, 'Search For', { text = lastsearch, replace = true } )

      if not sstr then return end

      local S = require 'pls-db'
      local fancysearch = S.megasearch( sstr )
      wv.log('debug','fancysearch=%s',fancysearch)
      lastsearch = sstr
      picklist( ('Search: ' .. sstr),
                'select ROWID,dt_modified,title from note where ' .. fancysearch .. string.format(' order by dt_modified desc limit %d',((env.curses.nrows-2)*6)) )
   end

   while true do
      cord.event.grab.wait()

      wv.log('debug','main window got grab key')

      menuedFunctions( cord, {
         New = function() cord.event.NewRecord() end,
         Goto = function() cord.event.OpenRecord()  end,
         PushFS = function() Winman.toggle_fullscreen()  end,
         Quit = function() cord.event.quit()        end,
         [{ '/', 'Search' }] = function() cord.event.Search() end,
      })

   end

end -- end, function entryfn_app













local function ding(x,p,...)
end

-- state variables to track whether a prefix is active
local command_prefix = false
local personal_prefix = false
local esc_prefix = false

-- Curses on unix systems generally don't support unique "Alt" sequences
-- like M-f, M-b, M-d which are so key to the emacs editing experience.
-- instead these are sent as escape sequences, ie, ESC followd by "f".
-- This means our main keyhandler has to "trap" the key immediately following
-- the escape and convert it to a unique "M-letter" style event.  But this
-- means that when escape is pressed we are _waiting_ for the next
-- keystroke and won't send the escape key immediately, which makes the escape
-- key almost useless.  This cord allows the escape key to be sent on by
-- itself if no follow-on key is received within a certain amount of time.
local cord_eschandler=Nylon.cord('escsender',
    function( cord )
       local count = 1
       local esccount
       cord.event.gotesc = function(k)
          esccount = count
          cord.event.sendesc()
       end
       cord.event.gotnonesc = function(k)
          count = count + 1
       end
       while true do
          cord.event.sendesc.wait()
          cord:sleep(0.1) -- wait 100 ms
          if esccount == count then -- no keys received since esc
             wv.log('debug','should send escape here')
             Winman.inject_key( 9027 ) -- ctrl+g
--             Winman.inject_key( 27 ) -- ctrl+g
          end
       end
    end)


local keymap_everybody       = Keys.everybody
local keymap_command_prefix  = Keys.command_prefix
local keymap_personal_prefix = Keys.personal_prefix
local keymap_esc_prefix      = Keys.esc_prefix

local function app_keymapper( k )
   
   local function handle_if_app_event_or_return( k )
      if cord_app:has_event(k) then
         cord_app.event[k]()
      else
         return k
      end
   end

   local function unknown(c)
                          if k >= 32 and k < 128 then
                             ding( 'Unknown prefix command Ctrl-%s + "%c"', c, k )
                          else
                             if k < 27 then
                                ding( 'Unknown prefix command Ctrl-%s + Ctrl-%c', c, (k+64) )
                             else
                                ding( 'Unknown prefix command Ctrl-%s + %d', c, k )
                             end
                          end
   end

   if k == 27 then -- esc
      esc_prefix = true
      cord_eschandler.event.gotesc()
      return
   else
      cord_eschandler.event.gotnonesc()
   end

   if k == 9027 then -- real escape; see cord_eschandler
      k = 27
   end

   if esc_prefix then
      esc_prefix = false
      if keymap_esc_prefix[k] then
         return app_keymapper( keymap_esc_prefix[k] )
      end
   end

   if command_prefix then
      command_prefix = false
      if keymap_command_prefix[k] then
         -- Map the sequence of keys (C-x s, C-x k, etc) to a named event. The named event
         -- string is then forwarded to key handlers just as an integer key value would be
         wv.log('debug','got cmd prefix mapped [%d=>%s]',k,keymap_command_prefix[k])
         return handle_if_app_event_or_return( keymap_command_prefix[k] )
      else
         unknown 'X'
      end
   elseif personal_prefix then
      personal_prefix = false
      return handle_if_app_event_or_return( keymap_personal_prefix[k] )
   elseif keymap_everybody[k] then
      return handle_if_app_event_or_return( keymap_everybody[k] )
   elseif k == 3 then -- begin personal prefix
      personal_prefix = true
      wv.log 'start personal prefix'
   elseif k == 435 then -- M-s
      wv.log 'got swap / M-s key'
      cord_app.event.SwapBuffers()
   elseif k == Keys.control.menu then -- C-l
      cord_app.event.grab()
   elseif k == 20 or k == 24 then -- C-t or C-x
      command_prefix = true
   else
      return k -- not handled or translated; pass on the original key
   end
end


local function app_unhandledkeys(k)
   wv.log('debug','app unhandled key handler key=%s',k)
   if k == 'jumptotag' then
      cord_app.event.OpenRecord()
   elseif k == 'toggle_landscape' then
      Winman.toggle_landscape()
   else
      wv.log('debug','unhandled key/input event=%s',k)
   end
end



------------------------------------------------------------------
------------------------------------------------------------------

local function cordfn_followup( cord )

   while true do
      
      local today = os.date("%y%m%d",os.time())
      local fupdate = string.format('%%followup%s%%', today)
      
      local followupCount = db:selectOne(
         'select count(*) as thecount from note where title like ? or detail like ?', fupdate, fupdate)
      
      wv.log('debug','followup search [%s] rc=%s', fupdate, json:encode(followupCount))
      
      if followupCount and tonumber(followupCount.thecount) > 0 then

         local manualClose = false
         local done = false
         picklist(
            { title = string.format('Follow Up %s', today),
              on = {
                 closed = function()
                    wv.log('debug','followup picklist manually closed')
                    manualClose = true
                    if done then
                       done()
                    end
                    done = function() end
                 end
            } },
            'select ROWID, dt_modified, title from note where title like ? or detail like ?', fupdate, fupdate)

         while (not done) and (today == os.date("%y%m%d", os.time())) do

            wv.log('debug', 'followup picklist open; wait until closed or new day')
            cord:sleep_manual( function(wakefn)
                                 done = function()
                                    done = false
                                    wakefn()
                                 end
                                 NylonSysCore.addOneShot( 120*1000, function() if done then done() end end) -- wake up every 2minutes to check for new day
            end)
         end
         
         if manualClose then
            wv.log('debug', 'followup picklist closed manually, wait 15min')
            cord:sleep(15*60*1000) -- 15 minutes
         end
         
      else
         cord:sleep(20) -- as long as no follow up items are found, check every 20s (to detect new ones)
      end
   end
end

Nylon.cord('followup', cordfn_followup)


------------------------------------------------------------------
------------------------------------------------------------------
Winman.create( env )


picklist(' Recent Edits ', 
--         string.format('select ROWID,dt_modified,title from note order by dt_modified desc limit %d', env.curses.nrows-2))
         string.format('select ROWID,dt_modified,title from note order by dt_modified desc limit %d', (env.curses.nrows*6)))

Winman.set_input( app_keymapper, app_unhandledkeys )

cord_app = Nylon.cord('app', entryfn_app, env)

cord_app.event.OpenRecord( recid )


------------------------------------------------------------------
------------------------------------------------------------------
Nylon.run()