summaryrefslogblamecommitdiff
path: root/lib/plugins/alcoolog.ex
blob: 9958889c4c07d7b7dc9c7b6d11d91fba71e7e157 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                  


                
                                         
 

                                                                                                                                                
                                                                        

                                                          
                                                               
                                                                
                                                                                                  



                                                                                                                                                  
                                                                                                                                     

                                                                                                    
 
                              


                          
     
 
            
 



                                                                                           
                                                      
 
     


                             





                                                                              

                                                                                                          


                                                                

                                                                                                  

                                                                         

                

                                                            
                                                                        
       
                                                                                        
                                                                
                                                                                                       
                                                                                                  
                                                                     

                                   









                                                                                              









                                                                                                   


                                                                                         
 

              
         

                                         
                    
                                               


                

                                                                                                                                          
                                                      


                     



                                                                                                                    
                                       
                           
                                      
                                                         
                                       
                              
                                                                                     
            
                                                                                   
           
                                          
                                                              
                                                               
                
                       
          



                                                            
                 
                                        




                                                          
                                                 

                                                         
                 
             
        
                                                                
                







                           




                                                                      




































                                                                                                                            

                                                      






















                                                                                                     
                                                                                                                         

                                                                                                                         



                            
                                                                                                                         
                                                                                                                  







                                                                                                                                     
                                             














                                                                                                                                                  
                                                                                                                                        














































                                                                                                                        



                                                         
                       




                                                   
           

                                               
 
                                                                       
                                                                             








                                                                                                                                         
 


                                        


                                                                                           

                                      

                                               


                                                            





                                                                                                                                               
                                                                                                                   
                                                          

                                                










                                                                           







                                                                                                                




                                                                             
 





                                                              
                                

                                                                                                                                                                 
                                                                                   
           
 





                                                   









                                                                                            
                                                                               









                                                                                                                                    








                                              









                                                                                     
                                 













                                                                          
                  
                                                                       



                                                                           
                                                                             
               

             
       

     





                                                                                                                      
                               
















                                                                                                    



                                                                                                    
                                                       

                                                    
                                                                                                     
                                               
                                                   






                                





                                                
                                                                                                         
                                                                    
                                                    
                                                             
                                                      














                                                       



                                                                                   
                                                     

                                       

















                                                                             








                                                                                                         


                                                         
                                                                                                                                                         
                              
                                                                     



                                                           

                                                                






                                                                                                                       

                                                                                             
























                                                                                                                

     
                                                                                                                        
                          
                                                                            
                     

       


                                                              
                                                    














                                                                                                                  
        
                            




                                                                                                                          

                                                                                             







                                                                                                    





                                                                                                                                         













                                























                                                                                                                              

                                                

     

                                              
                            


                                                                             
                                              










                                                                                                
 





                                                                                      
 

                                                     

     


                                           






                                                                             
                                                 

                                                                           
                                                           


                                                                                                
                                                                        








                                                                                      


                                         
 
                                                    


        

 
                                              

                                                                                                      
                                                

                                                                  
                                                                                
                                          

                                                                                                 
 
                                                                                 



                                                          
                                    
                                      



                                                              

                             
                                         



                                                                                                                           
                                             




























                                                                                                                                           
                                                     
                                                                                            
                                                

                                          
                                                               


                                            
                                                                
           





                                                                                                                          
                                                        
                                                                                  
                                            
                                                    
                                                                             
                                                           





                                                                                                   





                         
 
                                                                                                                             
                                      
                                                                                           
                           
       














                                                              
            













                                                                                      
           











                                                                                                                                                                             
                                                                                                           










                                                                                                                                




                     

                                                              
                                                                                                 












                                                                               
 





                                           
 
                                                                                                                                 

                                      

                                                                                      
     
 






                                                                                                  
                                                                   
                                                            
                                                                                                                                         



                                                                       
         


                     
 

                                                                      
                                                                                               









                                                                          
       
                     

     
                              
                                                              
                     

     








                                                 

                                                    

                                                                                                                                 






                                                                      
                                                            

                                
                                                            

                          





                                                          
                               
                                 




                                              
                                        






                                                                                                          
 

                                                         


       

                                                   
















                                                           



                                        

                                                     






                                                                                             
                                                    




                                              


                                                                  
                                                                                             







                                                                  

                                                                

                                                    
                                                                   







                                                                 

                                                                            








                                     

                                                                                             
                                                    




                                              

                                                                  
                                                                                    
                         
                                                                                                                                                                     


                                         
                         
                                                                              


                                                                               

                                                       
                                                                                                                         
















                                                                                                                          
                                                                                                                                





























                                                                                                         
                                   



                                                                                               
 





















                                                                         
   
defmodule Nola.Plugins.Alcoolog do
  require Logger

  @moduledoc """
  # [alcoolog]({{context_path}}/alcoolog)

  * **!santai `<cl | (calc)>` `<degrés d'alcool> [annotation]`**: enregistre un nouveau verre de `montant` d'une boisson à `degrés d'alcool`.
  * **!santai `<cl | (calc)>` `<beer name>`**: enregistre un nouveau verre de `cl` de la bière `beer name`, et checkin sur Untappd.com.
  * **!moar `[cl]` : enregistre un verre équivalent au dernier !santai.
  * **-santai**: annule la dernière entrée d'alcoolisme.
  * **.alcoolisme**: état du channel en temps réel.
  * **.alcoolisme `<semaine | Xj>`**: points par jour, sur X j.
  * **!alcoolisme `[pseudo]`**: affiche les points d'alcoolisme.
  * **!alcoolisme `[pseudo]` `<semaine | Xj>`**: affiche les points d'alcoolisme par jour sur X j.
  * **+alcoolisme `<h|f>` `<poids en kg>` `[facteur de perte en mg/l (10, 15, 20, 25)]`**: Configure votre profil d'alcoolisme.
  * **.sobre**: affiche quand la sobriété frappera sur le chan.
  * **!sobre `[pseudo]`**: affiche quand la sobriété frappera pour `[pseudo]`.
  * **!sobrepour `<date>`**: affiche tu pourras être sobre pour `<date>`, et si oui, combien de volumes d'alcool peuvent encore être consommés.
  * **!alcoolog**: ([voir]({{context_path}}/alcoolog)) lien pour voir l'état/statistiques et historique de l'alcoolémie du channel.
  * **!alcool `<cl>` `<degrés>`**: donne le nombre d'unités d'alcool dans `<cl>` à `<degrés>°`.
  * **!soif**: c'est quand l'apéro ?

  1 point = 1 volume d'alcool.

  Annotation: champ libre!

  ---

  ## `!txt`s

  * status utilisateur: `alcoolog.user_(sober|legal|legalhigh|high|toohigh|sick)(|_rising)`
  * mauvaises boissons: `alcoolog.drink_(negative|zero|negative)`
  * santo: `alcoolog.santo`
  * santai: `alcoolog.santai`
  * plus gros, moins gros: `alcoolog.(fatter|thinner)`

  """

  def irc_doc, do: @moduledoc

  def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)

  # tuple dets: {nick, date, volumes, current_alcohol_level, nom, commentaire}
  # tuple ets:  {{nick, date}, volumes, current, nom, commentaire}
  # tuple meta dets: {nick, map}
  #                   %{:weight => float, :sex => true(h),false(f)}
  @pubsub ~w(account)
  @pubsub_triggers ~w(santai moar again bis santo santeau alcoolog sobre sobrepour soif alcoolisme alcool)
  @default_user_meta %{weight: 77.4, sex: true, loss_factor: 15}

  def data_state() do
    dets_filename = (Nola.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
    dets_meta_filename = (Nola.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
    %{dets: dets_filename, meta: dets_meta_filename, ets: __MODULE__.ETS}
  end

  def init(_) do
    triggers = for(t <- @pubsub_triggers, do: "trigger:"<>t)
    for sub <- @pubsub ++ triggers do
      {:ok, _} = Registry.register(Nola.PubSub, sub, plugin: __MODULE__)
    end
    dets_filename = (Nola.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
    {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
    ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
    dets_meta_filename = (Nola.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
    {:ok, meta} = :dets.open_file(dets_meta_filename, [{:type,:set}])
    traverse_fun = fn(obj, dets) ->
      case obj do
        object = {nick, naive = %NaiveDateTime{}, volumes, active, cl, deg, name, comment} ->
          date = naive
          |> DateTime.from_naive!("Etc/UTC")
          |> DateTime.to_unix()
          new = {nick, date, volumes, active, cl, deg, name, comment, Map.new()}
          :dets.delete_object(dets, object)
          :dets.insert(dets, new)
          :ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, Map.new()})
          dets

        object = {nick, naive = %NaiveDateTime{}, volumes, active, cl, deg, name, comment, meta} ->
          date = naive
          |> DateTime.from_naive!("Etc/UTC")
          |> DateTime.to_unix()
          new = {nick, date, volumes, active, cl, deg, name, comment, Map.new()}
          :dets.delete_object(dets, object)
          :dets.insert(dets, new)
          :ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, Map.new()})
          dets

        object = {nick, date, volumes, active, cl, deg, name, comment, meta} ->
          :ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, meta})
          dets

        _ ->
          dets
      end
    end
    :dets.foldl(traverse_fun, dets, dets)
    :dets.sync(dets)
    state = %{dets: dets, meta: meta, ets: ets}
    {:ok, state}
  end

  @eau ["santo", "santeau"]
  def handle_info({:irc, :trigger, santeau, m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) when santeau in @eau do
    Nola.Plugins.Txt.reply_random(m, "alcoolog.santo")
    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "soif", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do
    now = DateTime.utc_now()
          |> Timex.Timezone.convert("Europe/Paris")
    apero = format_duration_from_now(%DateTime{now | hour: 18, minute: 0, second: 0}, false)
    day_of_week = Date.day_of_week(now)
    {txt, apero?} = cond do
      now.hour >= 0 && now.hour < 6 ->
        {["apéro tardif ? Je dis OUI ! SANTAI !"], true}
      now.hour >= 6 && now.hour < 12 ->
        if day_of_week >= 6 do
          {["de l'alcool pour le petit dej ? le week-end, pas de problème !"], true}
        else
          {["C'est quand même un peu tôt non ? Prochain apéro #{apero}"], false}
        end
      now.hour >= 12 && (now.hour < 14) ->
        {["oui! c'est l'apéro de midi! (et apéro #{apero})",
          "tu peux attendre #{apero} ou y aller, il est midi !"
        ], true}
      now.hour == 17 ->
        {[
          "ÇA APPROCHE !!! Apéro #{apero}",
          "BIENTÔT !!! Apéro #{apero}",
          "achetez vite les teilles, apéro dans #{apero}!",
          "préparez les teilles, apéro dans #{apero}!"
        ], false}
      now.hour >= 14 && now.hour < 18 ->
        weekend = if day_of_week >= 6 do
          " ... ou maintenant en fait, c'est le week-end!"
        else
          ""
        end
        {["tiens bon! apéro #{apero}#{weekend}",
          "courage... apéro dans #{apero}#{weekend}",
          "pas encore :'( apéro dans #{apero}#{weekend}"
        ], false}
      true ->
      {[
          "C'EST L'HEURE DE L'APÉRO !!! SANTAIIIIIIIIIIII !!!!"
        ], true}
    end

    txt = txt
          |> Enum.shuffle()
          |> Enum.random()

    m.replyfun.(txt)

    stats = get_full_statistics(state, m.account.id)
    if !apero? && stats.active > 0.1 do
      m.replyfun.("(... ou continue en fait, je suis pas ta mère !)")
    end

    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "sobrepour", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
    args = Enum.join(args, " ")
    {:ok, now} = DateTime.now("Europe/Paris", Tzdata.TimeZoneDatabase)
    time = case args do
      "demain " <> time ->
        {h, m} = case String.split(time, [":", "h"]) do
          [hour, ""] ->
            IO.puts ("h #{inspect hour}")
            {h, _} = Integer.parse(hour)
            {h, 0}
          [hour, min] when min != "" ->
            {h, _} = Integer.parse(hour)
            {m, _} = Integer.parse(min)
            {h, m}
          [hour] ->
            IO.puts ("h #{inspect hour}")
            {h, _} = Integer.parse(hour)
            {h, 0}
          _ -> {0, 0}
        end
        secs = ((60*60)*24)
        day = DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase)
        %DateTime{day | hour: h, minute: m, second: 0}
      "après demain " <> time ->
        secs = 2*((60*60)*24)
        DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase)
      datetime ->
        case Timex.Parse.DateTime.Parser.parse(datetime, "{}") do
          {:ok, dt} -> dt
          _ -> nil
        end
    end

    if time do
      meta = get_user_meta(state, m.account.id)
      stats = get_full_statistics(state, m.account.id)

      duration = round(DateTime.diff(time, now)/60.0)

      IO.puts "diff #{inspect duration} sober in #{inspect stats.sober_in}"

      if duration < stats.sober_in do
        int = stats.sober_in - duration
        m.replyfun.("désolé, aucune chance! tu seras sobre #{format_minute_duration(int)} après!")
      else
        remaining = duration - stats.sober_in
        if remaining < 30 do
          m.replyfun.("moins de 30 minutes de sobriété, c'est impossible de boire plus")
        else
          loss_per_minute = ((meta.loss_factor/100)/60)
          remaining_gl = (remaining-30)*loss_per_minute
          m.replyfun.("marge de boisson: #{inspect remaining} minutes, #{remaining_gl} g/l")
        end
      end

    end
    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do
    {:ok, token} = Nola.Token.new({:alcoolog, :index, m.sender.network, m.channel})
    url = NolaWeb.Router.Helpers.alcoolog_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel), token)
    m.replyfun.("-> #{url}")
    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :bang}}}, state) do
    url = NolaWeb.Router.Helpers.alcoolog_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel))
    m.replyfun.("-> #{url}")
    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "alcool", m = %IRC.Message{trigger: %IRC.Trigger{args: args = [cl, deg], type: :bang}}}, state) do
    {cl, _} = Util.float_paparse(cl)
    {deg, _} = Util.float_paparse(deg)
    points = Alcool.units(cl, deg)
    meta = get_user_meta(state, m.account.id)
    k = if meta.sex, do: 0.7, else: 0.6
    weight = meta.weight
    gl = (10*points)/(k*weight)
    duration = round(gl/((meta.loss_factor/100)/60))+30
        sober_in_s = if duration > 0 do
        duration = Timex.Duration.from_minutes(duration)
          Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
        else
          ""
        end

    m.replyfun.("Il y a #{Float.round(points+0.0, 4)} unités d'alcool dans #{cl}cl à #{deg}° (#{Float.round(gl + 0.0, 4)} g/l, #{sober_in_s})")
    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: [cl, deg | comment], type: :bang}}}, state) do
    santai(m, state, cl, deg, comment)
    {:noreply, state}
  end

  @moar [
    "{{message.sender.nick}}: la même donc ?",
    "{{message.sender.nick}}: et voilà la petite sœur !"
  ]

  def handle_info({:irc, :trigger, "bis", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
    handle_info({:irc, :trigger, "moar", m}, state)
  end
  def handle_info({:irc, :trigger, "again", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
    handle_info({:irc, :trigger, "moar", m}, state)
  end

  def handle_info({:irc, :trigger, "moar", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
    case get_statistics_for_nick(state, m.account.id) do
      {_, obj = {_, _date, _points, _active, cl, deg, _name, comment, _meta}} ->
        cl = case args do
          [cls] ->
            case Util.float_paparse(cls) do
              {cl, _} -> cl
              _ -> cl
            end
          _ -> cl
        end
        moar = @moar |> Enum.shuffle() |> Enum.random() |> Tmpl.render(m) |> m.replyfun.()
        santai(m, state, cl, deg, comment, auto_set: true)
      {_, obj = {_, date, points, _last_active, type, descr}} ->
        case Regex.named_captures(~r/^(?<cl>\d+[.]\d+)cl\s+(?<deg>\d+[.]\d+)°$/, type) do
          nil -> m.replyfun.("suce")
          u ->
            moar = @moar |> Enum.shuffle() |> Enum.random() |> Tmpl.render(m) |> m.replyfun.()
            santai(m, state, u["cl"], u["deg"], descr, auto_set: true)
        end
      _ -> nil
    end
    {:noreply, state}
  end

  defp santai(m, state, cl, deg, comment, options \\ []) do
    comment = cond do
      comment == [] -> nil
      is_binary(comment) -> comment
      comment == nil -> nil
      true -> Enum.join(comment, " ")
    end

    {cl, cl_extra} = case {Util.float_paparse(cl), cl} do
      {{cl, extra}, _} -> {cl, extra}
    {:error, "("<>_} ->
        try do
          {:ok, result} = Abacus.eval(cl)
          {result, nil}
        rescue
          _ -> {nil, "cl: invalid calc expression"}
        end
      {:error, _} -> {nil, "cl: invalid value"}
    end

    {deg, comment, auto_set, beer_id} = case Util.float_paparse(deg) do
      {deg, _} -> {deg, comment, Keyword.get(options, :auto_set, false), nil}
      :error ->
        beername = if(comment, do: "#{deg} #{comment}", else: deg)
        case Untappd.search_beer(beername, limit: 1) do
          {:ok, %{"response" => %{"beers" => %{"count" => count, "items" => [%{"beer" => beer, "brewery" => brewery} | _]}}}} ->
            {Map.get(beer, "beer_abv"), "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")}", true, Map.get(beer, "bid")}
          _ ->
              {deg, "could not find beer", false, nil}
        end
    end

    cond do
      cl == nil -> m.replyfun.(cl_extra)
      deg == nil -> m.replyfun.(comment)
      cl >= 500 || deg >= 100 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_toohuge")
      cl == 0 || deg == 0 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_zero")
      cl < 0 || deg < 0 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_negative")
      true ->
        points = Alcool.units(cl, deg)
        now = m.at || DateTime.utc_now()
              |> DateTime.to_unix(:millisecond)
        user_meta = get_user_meta(state, m.account.id)
        name = "#{cl}cl #{deg}°"
        old_stats = get_full_statistics(state, m.account.id)
        meta = %{}
        meta = Map.put(meta, "timestamp", now)
        meta = Map.put(meta, "weight", user_meta.weight)
        meta = Map.put(meta, "sex", user_meta.sex)
        :ok = :dets.insert(state.dets, {m.account.id, now, points, if(old_stats, do: old_stats.active, else: 0), cl, deg, name, comment, meta})
        true = :ets.insert(state.ets, {{m.account.id, now}, points, if(old_stats, do: old_stats.active, else: 0),cl, deg, name, comment, meta})
        #sante = @santai |> Enum.map(fn(s) -> String.trim(String.upcase(s)) end) |> Enum.shuffle() |> Enum.random()
        sante = Nola.Plugins.Txt.random("alcoolog.santai")
        k = if user_meta.sex, do: 0.7, else: 0.6
        weight = user_meta.weight
        peak = Float.round((10*points||0.0)/(k*weight), 4)
        stats = get_full_statistics(state, m.account.id)
        sober_add = if old_stats && Map.get(old_stats || %{}, :sober_in) do
          mins = round(stats.sober_in - old_stats.sober_in)
          " [+#{mins}m]"
        else
          ""
        end
        nonow = DateTime.utc_now()
        sober = nonow |> DateTime.add(round(stats.sober_in*60), :second)
              |> Timex.Timezone.convert("Europe/Paris")
        at = if nonow.day == sober.day do
          {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
          detail
        else
          {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
          detail
        end

        up = if stats.active_drinks > 1 do
          " " <> Enum.join(for(_ <- 1..stats.active_drinks, do: "▲")) <> ""
        else
          ""
        end

        since_str = if stats.since && stats.since_min > 180 do
        "(depuis: #{stats.since_s}) "
      else
        ""
      end

        msg = fn(nick, extra) ->
          "#{sante} #{nick} #{extra}#{up} #{format_points(points)} @#{stats.active}g/l [+#{peak} g/l]"
          <> " (15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) #{since_str}(sobriété #{at} (dans #{stats.sober_in_s})#{sober_add}) !"
          <> " (aujourd'hui #{stats.daily_volumes} points - #{stats.daily_gl} g/l)"
        end

        meta = if beer_id do
          Map.put(meta, "untappd:beer_id", beer_id)
        else
          meta
        end

        if beer_id do
          spawn(fn() ->
            case Untappd.maybe_checkin(m.account, beer_id) do
              {:ok, body} ->
                badges = get_in(body, ["badges", "items"])
                if badges != [] do
                  badges_s = Enum.map(badges, fn(badge) -> Map.get(badge, "badge_name") end)
                           |> Enum.filter(fn(b) -> b end)
                           |> Enum.intersperse(", ")
                           |> Enum.join("")
                    badge = if(length(badges) > 1, do: "badges", else: "badge")
                    m.replyfun.("\\O/ Unlocked untappd #{badge}: #{badges_s}")
                end
                :ok
              {:error, {:http_error, error}} when is_integer(error) -> m.replyfun.("Checkin to Untappd failed: #{to_string(error)}")
              {:error, {:http_error, error}} -> m.replyfun.("Checkin to Untappd failed: #{inspect error}")
              _ -> :error
            end
          end)
        end

        local_extra = if auto_set do
          if comment do
            " #{comment} (#{cl}cl @ #{deg}°)"
          else
            "#{cl}cl @ #{deg}°"
          end
        else
          ""
        end
        m.replyfun.(msg.(m.sender.nick, local_extra))
        notify = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
        for {net, chan} <- notify do
          user = IRC.UserTrack.find_by_account(net, m.account)
          nick = if(user, do: user.nick, else: m.account.name)
          extra = " " <> present_type(name, comment) <> ""
          IRC.Connection.broadcast_message(net, chan, msg.(nick, extra))
        end

        miss = cond do
          points <= 0.6 -> :small
          stats.active30m >= 2.9 && stats.active30m < 3 -> :miss3
          stats.active30m >= 1.9 && stats.active30m < 2 -> :miss2
          stats.active30m >= 0.9 && stats.active30m < 1 -> :miss1
          stats.active30m >= 0.45 && stats.active30m < 0.5 -> :miss05
          stats.active30m >= 0.20 && stats.active30m < 0.20 -> :miss025
          stats.active30m >= 3 && stats.active1h < 3.15 -> :small3
          stats.active30m >= 2 && stats.active1h < 2.15 -> :small2
          stats.active30m >= 1.5 && stats.active1h < 1.5 -> :small15
          stats.active30m >= 1 && stats.active1h < 1.15 -> :small1
          stats.active30m >= 0.5 && stats.active1h <= 0.51 -> :small05
          stats.active30m >= 0.25 && stats.active30m <= 0.255 -> :small025
          true -> nil
        end

        if miss do
          miss = Nola.Plugins.Txt.random("alcoolog.#{to_string(miss)}")
          if miss do
            for {net, chan} <- IRC.Membership.notify_channels(m.account) do
              user = IRC.UserTrack.find_by_account(net, m.account)
              nick = if(user, do: user.nick, else: m.account.name)
              IRC.Connection.broadcast_message(net, chan, "#{nick}: #{miss}")
            end
          end
        end
    end
  end

  def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do
    m.replyfun.("!santai <cl> <degrés> [commentaire]")
    {:noreply, state}
  end

  def get_all_stats() do
    Nola.Account.all_accounts()
    |> Enum.map(fn(account) -> {account.id, get_full_statistics(account.id)} end)
    |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
    |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
  end

  def get_channel_statistics(account, network, nil) do
    IRC.Membership.expanded_members_or_friends(account, network, nil)
    |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end)
    |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
    |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
  end

  def get_channel_statistics(_, network, channel), do: get_channel_statistics(network, channel)

  def get_channel_statistics(network, channel) do
    IRC.Membership.expanded_members(network, channel)
    |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end)
    |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
    |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
  end

  @spec since() :: %{Nola.Account.id() => DateTime.t()}
  @doc "Returns the last time the user was at 0 g/l"
  def since() do
    :ets.foldr(fn({{acct, timestamp_or_date}, _vol, current, _cl, _deg, _name, _comment, _m}, acc) ->
      if !Map.get(acc, acct) && current == 0 do
        date = Util.to_date_time(timestamp_or_date)
        Map.put(acc, acct, date)
      else
        acc
      end
    end, %{}, __MODULE__.ETS)
  end

  def get_full_statistics(nick) do
    get_full_statistics(data_state(), nick)
  end

  defp get_full_statistics(state, nick) do
    case get_statistics_for_nick(state, nick) do
      {count, {_, last_at, last_points, last_active, last_cl, last_deg, last_type, last_descr, _meta}} ->
        {active, active_drinks} = current_alcohol_level(state, nick)
        {_, m30} = alcohol_level_rising(state, nick)
        {rising, m15} = alcohol_level_rising(state, nick, 15)
        {_, m5} = alcohol_level_rising(state, nick, 5)
        {_, h1} = alcohol_level_rising(state, nick, 60)

        trend = if rising do
          "▲"
        else
          "▼"
        end
      user_state = cond do
        active <= 0.0 -> :sober
        active <= 0.25 -> :low
        active <= 0.50 -> :legal
        active <= 1.0 -> :legalhigh
        active <= 2.5 -> :high
        active < 3 -> :toohigh
        true -> :sick
        end

      rising_file_key = if rising, do: "_rising", else: ""
      txt_file = "alcoolog." <> "user_" <> to_string(user_state) <> rising_file_key
      user_status = Nola.Plugins.Txt.random(txt_file)

      meta = get_user_meta(state, nick)
        minutes_til_sober = h1/((meta.loss_factor/100)/60)
        minutes_til_sober = cond do
          active < 0 -> 0
          m15 < 0 -> 15
          m30 < 0 -> 30
          h1 < 0 -> 60
          minutes_til_sober > 0 ->
            Float.round(minutes_til_sober+60)
          true -> 0
        end

        duration = Timex.Duration.from_minutes(minutes_til_sober)
        sober_in_s = if minutes_til_sober > 0 do
          Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
        else
          nil
        end

        since = if active > 0 do
              since()
              |> Map.get(nick)
        end

        since_diff = if since, do: Timex.diff(DateTime.utc_now(), since, :minutes)
        since_duration = if since, do: Timex.Duration.from_minutes(since_diff)
        since_s = if since, do: Timex.Format.Duration.Formatter.lformat(since_duration, "fr", :humanized)

      {total_volumes, total_gl} = user_stats(state, nick)


        %{active: active, last_at: last_at, last_cl: last_cl, last_deg: last_deg, last_points: last_points, last_type: last_type, last_descr: last_descr,
          trend_symbol: trend,
          active5m: m5, active15m: m15, active30m: m30, active1h: h1,
          rising: rising,
          active_drinks: active_drinks,
          user_status: user_status,
          daily_gl: total_gl, daily_volumes: total_volumes,
          sober_in: minutes_til_sober, sober_in_s: sober_in_s,
          since: since, since_min: since_diff, since_s: since_s,
        }
    _ ->
      nil
    end
  end

  def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :dot}}}, state) do
    nicks = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
    |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end)
    |> Enum.filter(fn({_nick, status}) -> status && status.sober_in && status.sober_in > 0 end)
    |> Enum.sort_by(fn({_, status}) -> status.sober_in end, &</2)
    |> Enum.map(fn({nick, stats}) ->
      now = DateTime.utc_now()
      sober = now |> DateTime.add(round(stats.sober_in*60), :second)
            |> Timex.Timezone.convert("Europe/Paris")
        at = if now.day == sober.day do
          {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
          detail
        else
          {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
          detail
        end
      "#{nick} sobre #{at} (dans #{stats.sober_in_s})"
    end)
    |> Enum.intersperse(", ")
    |> Enum.join("")
    |> (fn(line) ->
      case line do
        "" -> "tout le monde est sobre......."
        line -> line
      end
    end).()
    |> m.replyfun.()
    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
    account = case args do
      [nick] -> Nola.Account.find_always_by_nick(m.network, m.channel, nick)
      [] -> m.account
    end

    if account do
      user = IRC.UserTrack.find_by_account(m.network, account)
      nick = if(user, do: user.nick, else: account.name)
      stats = get_full_statistics(state, account.id)
      if stats && stats.sober_in > 0 do
        now = DateTime.utc_now()
        sober = now |> DateTime.add(round(stats.sober_in*60), :second)
              |> Timex.Timezone.convert("Europe/Paris")
          at = if now.day == sober.day do
            {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
            detail
          else
            {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
            detail
          end
        m.replyfun.("#{nick} sera sobre #{at} (dans #{stats.sober_in_s})!")
      else
        m.replyfun.("#{nick} est déjà sobre. aidez le !")
      end
    else
      m.replyfun.("inconnu")
    end
    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :dot}}}, state) do
    nicks = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
    |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end)
    |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
    |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
    |> Enum.map(fn({nick, status}) ->
        trend_symbol = if status.active_drinks > 1 do
          Enum.join(for(_ <- 1..status.active_drinks, do: status.trend_symbol))
        else
          status.trend_symbol
        end
      since_str = if status.since_min > 180 do
        "depuis: #{status.since_s} | "
      else
        ""
      end
      "#{nick} #{status.user_status} #{trend_symbol} #{Float.round(status.active, 4)} g/l [#{since_str}sobre dans: #{status.sober_in_s}]"
    end)
    |> Enum.intersperse(", ")
    |> Enum.join("")

    msg = if nicks == "" do
      "wtf?!?! personne n'a bu!"
    else
      nicks
    end

    m.replyfun.(msg)
    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [time], type: :dot}}}, state) do
    time = case time do
      "semaine" -> 7
      string ->
        case Integer.parse(string) do
          {time, "j"} -> time
          {time, "J"} -> time
          _ -> nil
        end
    end

    if time do
      aday = time*((24 * 60)*60)
      now = DateTime.utc_now()
      before = now
               |> DateTime.add(-aday, :second)
               |> DateTime.to_unix(:millisecond)
      over_time_stats(before, time, m, state)
    else
      m.replyfun.(".alcooolisme semaine|Xj")
    end
    {:noreply, state}
  end

  def user_over_time(account, count) do
    user_over_time(data_state(), account, count)
  end

  def user_over_time(state, account, count) do
    delay = count*((24 * 60)*60)
    now = DateTime.utc_now()
    before = DateTime.utc_now()
             |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
             |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
             |> DateTime.to_unix(:millisecond)
    #[
#  {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
#   [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
    #]
    match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
       [{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]}
    ]
    :ets.select(state.ets, match)
    |> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) ->
      date = DateTime.from_unix!(ts, :millisecond)
             |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)

      date = if date.hour <= 8 do
        DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase)
      else
        date
      end
      |> DateTime.to_date()

      Map.put(acc, date, Map.get(acc, date, 0) + vol)
    end)
  end

  def user_over_time_gl(account, count) do
    state = data_state()
    meta = get_user_meta(state, account.id)
    delay = count*((24 * 60)*60)
    now = DateTime.utc_now()
    before = DateTime.utc_now()
             |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
             |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
             |> DateTime.to_unix(:millisecond)
    #[
#  {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
#   [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
    #]
    match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
       [{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]}
    ]
    :ets.select(state.ets, match)
    |> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) ->
      date = DateTime.from_unix!(ts, :millisecond)
             |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)

      date = if date.hour <= 8 do
        DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase)
      else
        date
      end
      |> DateTime.to_date()
      weight = meta.weight
      k = if meta.sex, do: 0.7, else: 0.6
      gl = (10*vol)/(k*weight)

      Map.put(acc, date, Map.get(acc, date, 0) + gl)
    end)
  end



  defp over_time_stats(before, j, m, state) do
    #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _, _, _, _}) when date > before -> obj end)
    match = [{{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_},
       [{:>, :"$1", {:const, before}}], [:"$_"]}
    ]
  # tuple ets:  {{nick, date}, volumes, current, nom, commentaire}
    members = IRC.Membership.members_or_friends(m.account, m.network, m.channel)
    drinks = :ets.select(state.ets, match)
    |> Enum.filter(fn({{account, _}, _, _, _, _, _, _, _}) -> Enum.member?(members, account) end)
             |> Enum.sort_by(fn({{_, ts}, _, _, _, _, _, _, _}) -> ts end, &>/2)

    top = Enum.reduce(drinks, %{}, fn({{nick, _}, vol, _, _, _, _, _, _}, acc) ->
      all = Map.get(acc, nick, 0)
      Map.put(acc, nick, all + vol)
    end)
    |> Enum.sort_by(fn({_nick, count}) -> count end, &>/2)
    |> Enum.map(fn({nick, count}) ->
      account = Nola.Account.get(nick)
      user = IRC.UserTrack.find_by_account(m.network, account)
      nick = if(user, do: user.nick, else: account.name)
      "#{nick}: #{Float.round(count, 4)}"
    end)
    |> Enum.intersperse(", ")

    m.replyfun.("sur #{j} jours: #{top}")
    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do
    meta = get_user_meta(state, m.account.id)
    hf = if meta.sex, do: "h", else: "f"
    m.replyfun.("+alcoolisme sexe: #{hf} poids: #{meta.weight} facteur de perte: #{meta.loss_factor}")
    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [h, weight | rest], type: :plus}}}, state) do
    h = case h do
      "h" -> true
      "f" -> false
      _ -> nil
    end

      weight = case Util.float_paparse(weight) do
        {weight, _} -> weight
        _ -> nil
      end

      {factor} = case rest do
        [factor] ->
          case Util.float_paparse(factor) do
            {float, _} -> {float}
            _ -> {@default_user_meta.loss_factor}
          end
        _ -> {@default_user_meta.loss_factor}
      end

      if h == nil || weight == nil do
        m.replyfun.("paramètres invalides")
      else
        old_meta = get_user_meta(state, m.account.id)
        meta = Map.merge(@default_user_meta, %{sex: h, weight: weight, loss_factor: factor})
        put_user_meta(state, m.account.id, meta)
        cond do
          old_meta.weight < meta.weight ->
            Nola.Plugins.Txt.reply_random(m, "alcoolog.fatter")
          old_meta.weight == meta.weight ->
            m.replyfun.("aucun changement!")
          true ->
            Nola.Plugins.Txt.reply_random(m, "alcoolog.thinner")
        end
      end

    {:noreply, state}
  end

  def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :minus}}}, state) do
    case get_statistics_for_nick(state, m.account.id) do
      {_, obj = {_, date, points, _last_active, _cl, _deg, type, descr, _meta}} ->
        :dets.delete_object(state.dets, obj)
        :ets.delete(state.ets, {m.account.id, date})
        m.replyfun.("supprimé: #{m.sender.nick} #{points} #{type} #{descr}")
        Nola.Plugins.Txt.reply_random(m, "alcoolog.delete")
        notify = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
        for {net, chan} <- notify do
          user = IRC.UserTrack.find_by_account(net, m.account)
          nick = if(user, do: user.nick, else: m.account.name)
          IRC.Connection.broadcast_message(net, chan, "#{nick} -santai #{points} #{type} #{descr}")
        end
        {:noreply, state}
      _ ->
        {:noreply, state}
    end
  end


  def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
    {account, duration} = case args do
      [nick | rest] -> {Nola.Account.find_always_by_nick(m.network, m.channel, nick), rest}
      [] -> {m.account, []}
    end
    if account do
      duration = case duration do
        ["semaine"] -> 7
        [j] ->
          case Integer.parse(j) do
            {j, "j"} -> j
            _ -> nil
          end
        _ -> nil
      end
      user = IRC.UserTrack.find_by_account(m.network, account)
      nick = if(user, do: user.nick, else: account.name)
      if duration do
        if duration > 90 do
          m.replyfun.("trop gros, ça rentrera pas")
        else
          # duration stats
          stats = user_over_time(state, account, duration)
          |> Enum.sort_by(fn({k,_v}) -> k end, {:asc, Date})
          |> Enum.map(fn({date, count}) ->
            "#{date.day}: #{Float.round(count, 2)}"
          end)
          |> Enum.intersperse(", ")
          |> Enum.join("")

          if stats == "" do
            m.replyfun.("alcoolisme a zéro sur #{duration}j :/")
          else
            m.replyfun.("alcoolisme de #{nick}, #{duration} derniers jours: #{stats}")
          end
        end
      else
        if stats = get_full_statistics(state, account.id) do
          trend_symbol = if stats.active_drinks > 1 do
            Enum.join(for(_ <- 1..stats.active_drinks, do: stats.trend_symbol))
          else
            stats.trend_symbol
          end
        # TODO: Lookup nick for account_id
          msg = "#{nick} #{stats.user_status} "
          <> (if stats.active > 0 || stats.active15m > 0 || stats.active30m > 0 || stats.active1h > 0, do: ": #{trend_symbol} #{Float.round(stats.active, 4)}g/l ", else: "")
          <> (if stats.active30m > 0 || stats.active1h > 0, do: "(15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) ", else: "")
          <> (if stats.sober_in > 0, do: "— Sobre dans #{stats.sober_in_s} ", else: "")
          <> (if stats.since && stats.since_min > 180, do: "— Paitai depuis #{stats.since_s} ", else: "")
          <> "— Dernier verre: #{present_type(stats.last_type, stats.last_descr)} [#{Float.round(stats.last_points+0.0, 4)}] "
          <> "#{format_duration_from_now(stats.last_at)} "
          <> (if stats.daily_volumes > 0, do: "— Aujourd'hui: #{stats.daily_volumes} #{stats.daily_gl}g/l", else: "")

          m.replyfun.(msg)
        else
          m.replyfun.("honteux mais #{nick} n'a pas l'air alcoolique du tout. /kick")
        end
      end
      else
      m.replyfun.("je ne connais pas cet utilisateur")
    end
    {:noreply, state}
  end


  # Account merge
  def handle_info({:account_change, old_id, new_id}, state) do
    spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
    Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
        Logger.debug("alcolog/account_change:: merging #{old_id} -> #{new_id}")
      rename_object_owner(table, state.ets, obj, old_id, new_id)
    end)
    case :dets.lookup(state.meta, {:meta, old_id}) do
      [{_, meta}] ->
        :dets.delete(state.meta, {:meta, old_id})
        :dets.insert(state.meta, {{:meta, new_id}, meta})
      _ ->
        :ok
    end
    {:noreply, state}
  end

  def terminate(_, state) do
    for dets <- [state.dets, state.meta] do
      :dets.sync(dets)
      :dets.close(dets)
    end
  end

  defp rename_object_owner(table, ets, object = {old_id, date, volume, current, cl, deg, name, comment, meta}, old_id, new_id) do
    :dets.delete_object(table, object)
    :ets.delete(ets, {old_id, date})
    :dets.insert(table, {new_id, date, volume, current, cl, deg, name, comment, meta})
    :ets.insert(ets, {{new_id, date}, volume, current, cl, deg, name, comment, meta})
  end

  # Account: move from nick to account id
  def handle_info({:accounts, accounts}, state) do
    #for x={:account, _, _, _, _} <- accounts, do: handle_info(x, state)
    #{:noreply, state}
    mapping = Enum.reduce(accounts, Map.new, fn({:account, _net, _chan, nick, account_id}, acc) ->
      Map.put(acc, String.downcase(nick), account_id)
    end)
    spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [], [:"$_"]}]
      Logger.debug("accounts:: mappings #{inspect mapping}")
    Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj = {nick, _date, _vol, _cur, _cl, _deg, _name, _comment, _meta}) ->
      #Logger.debug("accounts:: item #{inspect(obj)}")
      if new_id = Map.get(mapping, nick) do
        Logger.debug("alcolog/accounts:: merging #{nick} -> #{new_id}")
        rename_object_owner(table, state.ets, obj, nick, new_id)
      end
    end)
    {:noreply, state}
  end

  def handle_info({:account, _net, _chan, nick, account_id}, state) do
    nick = String.downcase(nick)
    spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
    Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
      Logger.debug("alcoolog/account:: merging #{nick} -> #{account_id}")
      rename_object_owner(table, state.ets, obj, nick, account_id)
    end)
    case :dets.lookup(state.meta, {:meta, nick}) do
      [{_, meta}] ->
        :dets.delete(state.meta, {:meta, nick})
        :dets.insert(state.meta, {{:meta, account_id}, meta})
      _ ->
        :ok
    end
    {:noreply, state}
  end

  def handle_info(t, state) do
    Logger.debug("#{__MODULE__}: unhandled info #{inspect t}")
    {:noreply, state}
  end

  def nick_history(account) do
    spec = [
      {{{:"$1", :_}, :_, :_, :_, :_, :_, :_, :_},
      [{:==, :"$1", {:const, account.id}}],
      [:"$_"]}
    ]
    :ets.select(data_state().ets, spec)
  end

  defp get_statistics_for_nick(state, account_id) do
    qvc = :dets.lookup(state.dets, account_id)
          |> Enum.sort_by(fn({_, ts, _, _, _, _, _, _, _}) -> ts end, &</2)
    count = Enum.reduce(qvc, 0, fn({_nick, _ts, points, _active, _cl, _deg, _type, _descr, _meta}, acc) -> acc + (points||0) end)
    last = List.last(qvc) || nil
    {count, last}
  end

  def present_type(type, descr) when descr in [nil, ""], do: "#{type}"
  def present_type(type, description), do: "#{type} (#{description})"

  def format_points(int) when is_integer(int) and int > 0 do
    "+#{Integer.to_string(int)}"
  end
  def format_points(int) when is_integer(int) and int < 0 do
    Integer.to_string(int)
  end
  def format_points(int) when is_float(int) and int > 0 do
    "+#{Float.to_string(Float.round(int,4))}"
  end
  def format_points(int) when is_float(int) and int < 0 do
    Float.to_string(Float.round(int,4))
  end
  def format_points(0), do: "0"
  def format_points(0.0), do: "0"

  defp format_relative_timestamp(timestamp) do
    alias Timex.Format.DateTime.Formatters
    alias Timex.Timezone
    date = timestamp
    |> DateTime.from_unix!(:millisecond)
    |> Timezone.convert("Europe/Paris")

    {:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
    {:ok, detail} = Formatters.Default.lformat(date, " ({h24}:{m})", "fr")

    relative <> detail
  end

  defp put_user_meta(state, account_id, meta) do
    :dets.insert(state.meta, {{:meta, account_id}, meta})
    :ok
  end

  defp get_user_meta(%{meta: meta}, account_id) do
    case :dets.lookup(meta, {:meta, account_id}) do
      [{{:meta, _}, meta}] ->
        Map.merge(@default_user_meta, meta)
      _ ->
        @default_user_meta
    end
  end
  # Calcul g/l actuel:
  # 1. load user meta
  # 2. foldr ets
  #   for each object
  #     get_current_alcohol
  #         ((object g/l) - 0,15/l/60)* minutes_since_drink
  #         if minutes_since_drink < 10, reduce g/l (?!)
  #     acc + current_alcohol
  #   stop folding when ?
  #

  def user_stats(account) do
    user_stats(data_state(), account.id)
  end

  defp user_stats(state = %{ets: ets}, account_id) do
    meta = get_user_meta(state, account_id)
    aday = (10 * 60)*60
    now = DateTime.utc_now()
    before = now
    |> DateTime.add(-aday, :second)
    |> DateTime.to_unix(:millisecond)
    #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
    match = [
      {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
       [
         {:>, :"$2", {:const, before}},
         {:"=:=", {:const, account_id}, :"$1"}
       ], [:"$_"]}
    ]
  # tuple ets:  {{nick, date}, volumes, current, nom, commentaire}
    drinks = :ets.select(ets, match)
    # {date, single_peak}
    total_volume = Enum.reduce(drinks, 0.0, fn({{_, date}, volume, _, _, _, _, _, _}, acc) ->
      acc + volume
    end)
    k = if meta.sex, do: 0.7, else: 0.6
    weight = meta.weight
    gl = (10*total_volume)/(k*weight)
    {Float.round(total_volume + 0.0, 4), Float.round(gl + 0.0, 4)}
  end

  defp alcohol_level_rising(state, account_id, minutes \\ 30) do
    {now, _} = current_alcohol_level(state, account_id)
    soon_date = DateTime.utc_now
                |> DateTime.add(minutes*60, :second)
    {soon, _} = current_alcohol_level(state, account_id, soon_date)
    soon = cond do
      soon < 0 -> 0.0
      true -> soon
    end
    #IO.puts "soon #{soon_date} - #{inspect soon} #{inspect now}"
    {soon > now, Float.round(soon+0.0, 4)}
  end

  defp current_alcohol_level(state = %{ets: ets}, account_id, now \\ nil) do
    meta = get_user_meta(state, account_id)
    aday = ((24*7) * 60)*60
    now = if now do
      now
    else
      DateTime.utc_now()
    end
    before = now
    |> DateTime.add(-aday, :second)
    |> DateTime.to_unix(:millisecond)
    #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
    match = [
      {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
       [
         {:>, :"$2", {:const, before}},
         {:"=:=", {:const, account_id}, :"$1"}
       ], [:"$_"]}
    ]
  # tuple ets:  {{nick, date}, volumes, current, nom, commentaire}
    drinks = :ets.select(ets, match)
             |> Enum.sort_by(fn({{_, date}, _, _, _, _, _, _, _}) -> date end, &</2)
    # {date, single_peak}
    {all, last_drink_at, gl, active_drinks} = Enum.reduce(drinks, {0.0, nil, [], 0}, fn({{_, date}, volume, _, _, _, _, _, _}, {all, last_at, acc, active_drinks}) ->
      k = if meta.sex, do: 0.7, else: 0.6
      weight = meta.weight
      peak = (10*volume)/(k*weight)
      date = case date do
               ts when is_integer(ts) -> DateTime.from_unix!(ts, :millisecond)
               date = %NaiveDateTime{} -> DateTime.from_naive!(date, "Etc/UTC")
               date = %DateTime{} -> date
             end
      last_at = last_at || date
      mins_since = round(DateTime.diff(now, date)/60.0)
      #IO.puts "Drink: #{inspect({date, volume})} - mins since: #{inspect mins_since} - last drink at #{inspect last_at}"
      # Apply loss since `last_at` on `all`
      #
      all = if last_at do
        mins_since_last = round(DateTime.diff(date, last_at)/60.0)
        loss = ((meta.loss_factor/100)/60)*(mins_since_last)
        #IO.puts "Applying last drink loss: from #{all}, loss of #{inspect loss} (mins since #{inspect mins_since_first})"
        cond do
          (all-loss) > 0 -> all - loss
          true -> 0.0
        end
      else
        all
      end
            #IO.puts "Applying last drink current before drink: #{inspect all}"
      if mins_since < 30 do
        per_min = (peak)/30.0
        current = (per_min*mins_since)
          #IO.puts "Applying current drink 30m: from #{peak}, loss of #{inspect per_min}/min (mins since #{inspect mins_since})"
        {all + current, date, [{date, current} | acc], active_drinks + 1}
      else
        {all + peak, date, [{date, peak} | acc], active_drinks}
      end
    end)
    #IO.puts "last drink #{inspect last_drink_at}"
    mins_since_last = if last_drink_at do
      round(DateTime.diff(now, last_drink_at)/60.0)
    else
      0
    end
    # Si on a déjà bu y'a déjà moins 15 minutes (big up le binge drinking), on applique plus de perte
    level = if mins_since_last > 15 do
      loss = ((meta.loss_factor/100)/60)*(mins_since_last)
      Float.round(all - loss, 4)
    else
      all
    end
    #IO.puts "\n LEVEL #{inspect level}\n\n\n\n"
    cond do
      level < 0 -> {0.0, 0}
      true -> {level, active_drinks}
    end
  end

  defp format_duration_from_now(date, with_detail \\ true) do
    date = if is_integer(date) do
      date = DateTime.from_unix!(date, :millisecond)
      |> Timex.Timezone.convert("Europe/Paris")
    else
      Util.to_naive_date_time(date)
    end
    now = DateTime.utc_now()
    |> Timex.Timezone.convert("Europe/Paris")
    {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(date, "({h24}:{m})", "fr")

    mins_since = round(DateTime.diff(now, date)/60.0)
    if ago = format_minute_duration(mins_since) do
      word = if mins_since > 0 do
        "il y a "
      else
        "dans "
      end
      word <> ago <> if(with_detail, do: " #{detail}", else: "")
    else
      "maintenant #{detail}"
    end
  end

  defp format_minute_duration(minutes) do
    sober_in_s = if (minutes != 0) do
      duration = Timex.Duration.from_minutes(minutes)
      Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
    else
      nil
    end
  end

end