table stash : { Todos : serialized (list {Title : string, Completed : bool}) }

val enter = 13
val escape = 27

(** A helper higher-order function, to map over a list with a function that is passed
  * a function for computing the list without the current element. *)
fun mapX [a] (f : (unit -> list a) -> a -> xbody) (ls : list a) : xbody =
    let
        fun mapX' ls acc =
            case ls of
                [] => <xml/>
              | x :: ls => <xml>{f (fn () => List.revAppend acc ls) x}{mapX' ls (x :: acc)}</xml>
    in
        mapX' ls []
    end

fun newTodo r =
    title <- source r.Title;
    completed <- source r.Completed;
    editing <- source False;
    underMouse <- source False;
    return {Title = title, Completed = completed, Editing = editing, UnderMouse = underMouse}

fun saver ls =
    dml (DELETE FROM stash WHERE TRUE);
    dml (INSERT INTO stash (Todos) VALUES ({[serialize ls]}))

fun saveLoop todos =
    ts <- get todos;
    ls <- List.mapM (fn r =>
                        title <- get r.Title;
                        completed <- get r.Completed;
                        return {Title = title, Completed = completed}) ts;
    rpc (saver ls);
    sleep 5000;
    saveLoop todos

fun main () =
    todos <- oneOrNoRows1 (SELECT * FROM stash);
    todos <- (case todos of
                  None => source []
                | Some r =>
                  ls <- List.mapM newTodo (deserialize r.Todos);
                  source ls);

    mainInputId <- fresh;
    mainInput <- source "";

    toggleAllId <- fresh;
    toggleAll <- source False;

    filterMode <- source Template.All;

    editingInputId <- fresh;
    editingInput <- source "";

    return (Template.document
            {Onload = (giveFocus mainInputId; saveLoop todos),
             Body = <xml>
               {Template.mainInput {Id = mainInputId,
                                    Source = mainInput,
                                    Onkeydown = fn ev => if ev.KeyCode = enter then
                                                             title <- get mainInput;
                                                             set mainInput "";
                                                             title <- return (String.trim title);
                                                             if title = "" then
                                                                 return ()
                                                             else
                                                                 t <- newTodo {Title = title, Completed = False};
                                                                 ts <- get todos;
                                                                 set todos (List.append ts (t :: []))
                                                         else
                                                             return ()}}
               <dyn signal={ts <- signal todos;
                            case ts of
                                [] => return <xml/>
                              | _ => return <xml>
                                {Template.mainSection
                                     {ToggleAll = {Id = toggleAllId,
                                                   Source = toggleAll,
                                                   Onchange = (b <- get toggleAll;
                                                               ts <- get todos;
                                                               List.app (fn it => set it.Completed b) ts)},
                                      Todos = mapX (fn del it => <xml>
                                        <dyn signal={mode <- signal filterMode;
                                                     completed <- signal it.Completed;
                                                     editng <- signal it.Editing;
                                                     return (if (completed && mode = Template.Active)
                                                                || (not completed && mode = Template.Completed) then
                                                                 <xml/>
                                                             else if editng then
                                                                 let
                                                                     val activated =
                                                                         text <- get editingInput;
                                                                         text <- return (String.trim text);
                                                                         if text = "" then
                                                                             set todos (del ())
                                                                         else
                                                                             set it.Title text;
                                                                             set it.Editing False
                                                                 in
                                                                     Template.editingTodo {Id = editingInputId,
                                                                                           Source = editingInput,
                                                                                           Onkeydown = fn ev =>
                                                                                                          if ev.KeyCode = enter then
                                                                                                              activated
                                                                                                          else if ev.KeyCode = escape then
                                                                                                              set it.Editing False
                                                                                                          else
                                                                                                              return (),
                                                                                           Onblur = activated}
                                                                 end
                                                             else
                                                                 Template.notEditingTodo {Onmouseover = set it.UnderMouse True,
                                                                                          Onmouseout = set it.UnderMouse False,
                                                                                          Ondblclick = (text <- get it.Title; set editingInput text; set it.Editing True),
                                                                                          Source = it.Completed,
                                                                                          Onchange = (ts <- get todos;
                                                                                                      allComplete <- List.foldlM (fn it acc =>
                                                                                                                                     completed <- get it.Completed;
                                                                                                                                     return (completed && acc)) True ts;
                                                                                                      set toggleAll allComplete),
                                                                                          Title = <xml><dyn signal={t <- signal it.Title; return (txt t)}/></xml>,
                                                                                          Destroy = <xml>
                                                                                            <dyn signal={um <- signal it.UnderMouse;
                                                                                                         return (if um then
                                                                                                                     Template.destroyButton {Onclick = set todos (del ())}
                                                                                                                 else
                                                                                                                     <xml/>)}/>
                                                                                          </xml>})}/>
                                        </xml>) ts}}

                                <dyn signal={(completed, remaining) <-
                                             List.foldlM (fn it (completed, remaining) =>
                                                             comped <- signal it.Completed;
                                                             return (if comped then
                                                                         (completed+1, remaining)
                                                                     else
                                                                         (completed, remaining+1)))
                                                         (0, 0) ts;

                                             return (Template.filter {Source = filterMode,
                                                                      Remaining = remaining,
                                                                      Completed = completed,
                                                                      ClearCompleted = (ts <- get todos;
                                                                                        ts' <- List.filterM (fn it =>
                                                                                                                b <- get it.Completed;
                                                                                                                return (not b)) ts;
                                                                                        set todos ts';
                                                                                        set toggleAll False)})}/>
                              </xml>}/>
    </xml>})