-- ***********************************************************************
--
-- Copyright 2016 by Sean Conner.
--
-- This program is free software: you can redistribute it and/or modify it
-- under the terms of the GNU General Public License as published by the
-- Free Software Foundation, either version 3 of the License, or (at your
-- option) any later version.
--
-- This program is distributed in the hope that it will be useful, but
-- WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
-- Public License for more details.
--
-- You should have received a copy of the GNU General Public License along
-- with this program.  If not, see <http://www.gnu.org/licenses/>.
--
-- Comments, questions and criticisms can be sent to: [email protected]
--
-- =======================================================================
--
-- Code to handle blog requests.
--
-- ***********************************************************************
-- luacheck: globals config display last_link init
-- luacheck: ignore 611

local exit    = require "org.conman.const.exit"
local syslog  = require "org.conman.syslog"
local process = require "org.conman.process"
local fsys    = require "org.conman.fsys"
local date    = require "org.conman.date"
local lpeg    = require "lpeg"
local io      = require "io"
local os      = require "os"
local table   = require "table"
local string  = require "string"

local setfenv  = setfenv
local require  = require
local loadfile = loadfile
local tonumber = tonumber
local tostring = tostring
local ipairs   = ipairs
local config   = config
local _VERSION = _VERSION

local blog = { require = require }

if _VERSION == "Lua 5.1" then
 module("blog")
else
 _ENV = {}
end

-- ***********************************************************************
-- usage:       date = read_date(fname)
-- desc:        Read the blog start and end date files.
-- input:       fname (string) either '.first' or '.last'
-- return:      date (table)
--                      * year
--                      * month
--                      * day
-- ***********************************************************************

local number    = lpeg.R"09"^1 / tonumber
local dateparse = lpeg.Ct(
                              lpeg.Cg(number,"year")  * lpeg.P"/"
                            * lpeg.Cg(number,"month") * lpeg.P"/"
                            * lpeg.Cg(number,"day")   * lpeg.P"."
                            * lpeg.Cg(number,"part")
                          )

local function read_date(fname)
 local f = io.open(fname,"r")
 local d = f:read("*l")
 return dateparse:match(d)
end

-- ***********************************************************************
-- usage:       titles = get_days_titles(when)
-- desc:        Retreive the titles of the posts of a given day
-- input:       when (table)
--                      * year
--                      * month
--                      * day
--                      * part
-- return:      titles (string/array) titles for each post
-- ***********************************************************************

local function get_days_titles(when)
 local res = {}
 local fname = string.format("%d/%02d/%02d/titles",when.year,when.month,when.day)

 if fsys.access(fname,"r") then
   for title in io.lines(fname) do
     table.insert(res,title)
   end
 end
 return res
end

-- ***********************************************************************
-- usage:       collect_day(formats,when)
-- desc:        Create gopher links for a day's entry
-- input:       formats (function array) functions required:
--                      * INFO  - format a INFO line
--                      * FILE  - format a FILE line
--                      * DIR   - format a DIR line
--                      * ERROR - format an ERROR line
--                      * HTML  - format an HTML line
--              when (table)
--                      * year
--                      * month
--                      * day
-- ***********************************************************************

local function collect_day(formats,when)
 local acc   = {}
 when.part   = 1
 local fname = string.format("%d/%02d/%02d/titles",when.year,when.month,when.day)

 if fsys.access(fname,"r") then
   for title in io.lines(fname) do
     local link = string.format("Phlog:%d/%02d/%02d.%d",when.year,when.month,when.day,when.part)
     table.insert(acc,{ formats.FILE , title , link })
     when.part = when.part + 1
   end
 end
 return acc
end

-- ***********************************************************************
-- usage:       collect_month(acc,formats,when)
-- desc:        Create gopher links for a month's worth of entries
-- input:       acc (table) table for accumulating links
--              formats (function array) functions required:
--                      * INFO  - format a INFO line
--                      * FILE  - format a FILE line
--                      * DIR   - format a DIR line
--                      * ERROR - format an ERROR line
--                      * HTML  - format an HTML line
--              when (table)
--                      * year
--                      * month
--                      * day
-- ***********************************************************************

local function collect_month(acc,formats,when)
 when.day = 1
 local d = os.time(when)
 table.insert(acc, { formats.INFO , os.date("%B, %Y",d) })
 local maxday = date.daysinmonth(when)

 for day = 1 , maxday do
   when.day    = day
   local posts = collect_day(formats,when)

   if #posts > 0 then
     local title = string.format("%d/%02d/%02d",when.year,when.month,when.day)
     local link  = string.format("Phlog:%d/%02d/%02d",when.year,when.month,when.day)
     table.insert(acc,{ formats.DIR , title , link })

     for _,post in ipairs(posts) do
       table.insert(acc,post)
     end
   end
 end
end

-- ***********************************************************************
-- LPEG code to parse a request.  tumber() will parse the request and return
-- a table with the following fields:
--
--      * year  - year of request
--      * month - month of request
--      * day   - day of request
--      * part  - part of day
--      * file  - file reference
--      * unit  - one of 'none', 'year', 'month' , 'day' , 'part' , 'file'
--                indicating how much of a request was made.
-- ***********************************************************************

local Ct = lpeg.Ct
local Cg = lpeg.Cg
local Cc = lpeg.Cc
local R  = lpeg.R
local P  = lpeg.P

local eos     = P(-1)
local file    = P"/" * Cg(P(1)^0,"file")  * Cg(Cc('file'), "unit")
local part    = P"." * Cg(number,"part")  * Cg(Cc('part'), "unit")
local day     = P"/" * Cg(number,"day")   * Cg(Cc('day'),  "unit")
local month   = P"/" * Cg(number,"month") * Cg(Cc('month'),"unit")
local year    =        Cg(number,"year")  * Cg(Cc('year'), "unit")
local tumbler = Ct(
                     year * month * day * file       * eos
                   + year * month * day * part       * eos
                   + year * month * day              * eos
                   + year * month * P"/"^-1          * eos
                   + year * P"/"^-1                  * eos
                   + Cg(Cc('none'),"unit")           * eos
                 )

-- ***********************************************************************
-- usage:       links = display(formats,request)
-- desc:        Return a list of gopher links for a given request
-- input:       formats (function array) functions required:
--              formats (function array) functions required:
--                      * INFO  - format a INFO line
--                      * FILE  - format a FILE line
--                      * DIR   - format a DIR line
--                      * ERROR - format an ERROR line
--                      * HTML  - format an HTML line
--              request (string) requested entry/ies
-- return:      links (array) array of gopher links
-- ***********************************************************************

       -- -----------------------------------------------------------------
       -- I'm using Lynx to generate the page view, and since I'm
       -- referencing the file directly, any local links get a file: URL,
       -- which needs to change.  I have the information to do that, but
       -- only when the blog configuration file is read in (because of the
       -- way LPeg works).  So this is a forware reference to the code to
       -- fix the links, which is defined in the init() method below.
       -- -----------------------------------------------------------------
local fix_local_links

function display(formats,request)
 local what = tumbler:match(request)

 if not what then
   return { { formats.ERROR , "Not found" , request } }
 end

 if what.unit == 'none' then
   local first = read_date(".first")
   local last  = read_date(".last")

   local year = {} -- luacheck: ignore

   for i = last.year , first.year , -1 do
     table.insert(year,{ formats.DIR , tostring(i) , "Phlog:" .. i})
   end

   return year

 elseif what.unit == 'year' then
   local first  = read_date(".first")
   local last   = read_date(".last")
   local months = {}
   local when   = { year = 1999 , month = 1 , day = 1 }

   for i = 1 , 12 do
     if what.year == first.year and i         >= first.month
     or what.year == last.year  and i         <= last.month
     or what.year >  first.year and what.year < last.year
     then
       when.month = i
       local d    = os.time(when)
       table.insert(
         months,
         {
           formats.DIR ,
           os.date("%B",d) ,
           string.format("Phlog:%d/%02d",what.year,i)
         }
       )
     end
   end

   return months

 elseif what.unit == 'month' then
   local days = {}
   collect_month(days,formats,what)
   return days

 elseif what.unit == 'day' then
   return collect_day(formats,what)

 elseif what.unit == 'part' then
   local titles = get_days_titles(what)

   if #titles > 0 then
     local cmd  = string.format(
                    "lynx -assume_local_charset=UTF-8 -assume_charset=UTF-8 -assume_unrec_charset=UTF-8 -force_html -dump %d/%02d/%02d/%d", -- luacheck: ignore
                    what.year,
                    what.month,
                    what.day,
                    what.part
                  )
     local lynx = io.popen(cmd,"r")
     local data = lynx:read("*a")
     lynx:close()
     data = fix_local_links:match(data)
     return titles[what.part] .. "\n" .. data
   else
     return "[Apparently, there's nothing here. ---Editor]"
   end

 elseif what.unit == 'file' then
   local filename = string.format("%d/%02d/%02d/%s",
               what.year,
               what.month,
               what.day,
               what.file)
   if what.file:match "%.gif$"
   or what.file:match "%.jpg$"
   or what.file:match "%.png$" then
     local f,err = io.open(filename,"rb")
     if f then
       return f,false
     else
       return nil,err
     end
   else
     local f,err = io.open(filename,"r")
     if f then
       return f,true
     else
       return nil,err
     end
   end

 else
   syslog('error',"Um ... what now?")
   return "[Well, this is unexpected!]"
 end
end

-- ***********************************************************************
-- usage:       link = last_link()
-- desc:        return a gopher link for the latest blog entry
-- return:      link (string) gopher link
-- ***********************************************************************

function last_link()
 local last = read_date(".last")
 local link = string.format(
               "Phlog:%d/%02d/%02d.%d",
               last.year,
               last.month,
               last.day,
               last.part
       )
 return link
end

-- ***********************************************************************

local format_unit =
{
 year = function(t)
   return string.format(
       "%s1Phlog:%d\r\n      %s%d",
       config.url,
       t.year,
       blog.url,
       t.year
   )
 end,

 month = function(t)
   return string.format(
       "%s1Phlog:%d/%02d\r\n      %s%d/%02d",
       config.url,
       t.year,
       t.month,
       blog.url,
       t.year,
       t.month
   )
 end,

 day = function(t)
   return string.format(
       "%s1Phlog:%d/%02d/%02d\r\n      %s%d/%02d/%02d",
       config.url,
       t.year,
       t.month,
       t.day,
       blog.url,
       t.year,
       t.month,
       t.day
   )
 end,

 part = function(t)
   return string.format(
       "%s0Phlog:%d/%02d/%02d.%d\r\n      %s%d/%02d/%02d.%d",
       config.url,
       t.year,
       t.month,
       t.day,
       t.part,
       blog.url,
       t.year,
       t.month,
       t.day,
       t.part
   )
 end,

 file = function(t)
   local st

   if t.file:match "%.gif$"
   or t.file:match "%.jpg$"
   or t.file:match "%.png$" then
     st = 'I'
   else
     st = '0'
   end

   return string.format(
       "%s%sPhlog:%d/%02d/%02d/%s\r\n      %s%d/%02d/%02d/%s",
       config.url,
       st,
       t.year,
       t.month,
       t.day,
       t.file,
       blog.url,
       t.year,
       t.month,
       t.day,
       t.file
   )
 end,
}

-- ***********************************************************************

local function affiliates(list)
 local pattern = P(false)
 for _,scheme in ipairs(list) do
   pattern = pattern
           + P(scheme.proto) * P":"
           * lpeg.C(R("!!","#~")^1)
           / function(c)
               return string.format(scheme.link,c)
             end
 end

 return pattern
end

-- ***********************************************************************
-- usage:       init()
-- desc:        Intialize the handler module
-- ***********************************************************************

function init()
 local f,err = loadfile(config.blog,"t",blog)
 if not f then
   syslog('critical',"%s: %s",config.blog,err)
   process.exit(exit.CONFIG)
 end

 if _VERSION == "Lua 5.1" then
   setfenv(f,blog)
 end

 f()
 fsys.chdir(blog.basedir)

 -- ------------------------------------------------------------------
 -- I'm using Lynx to format the entries.  For local links, they come out
 -- looking like:
 --
 --   file://localhost/home/spc/web/boston/journal/1999/12/15/1999/12/15.2
 -- or
 --   file://localhost/home/spc/web/boston/journal/1999/12/15/code.txt
 --
 -- This rather complicated looking LPeg expression does a substitution
 -- capture, transforming the above links to:
 --
 --    3. gopher://lucy.roswell.area51:7070/0Phlog:1999/12/15.2
 --       http://boston.roswell.area51/1999/12/15.2
 -- or
 --
 --   4. gopher://lucy.roswell.area51:7070/0Phlog:1999/12/15/code.txt
 --      http://boston.roswell.area51/1999/12/15/code.txt
 --
 -- The first portion does #3, the next portion #4 and the final
 -- portion (one line) just keeps the data flowing.
 -- ------------------------------------------------------------------

 fix_local_links = lpeg.Cs(( -- first portion
       (
       lpeg.C(P"file://localhost"
       * P(blog.basedir)
       * P"/"
       * R"09"^1 * P"/"
       * R"09"^1 * P"/"
       * R"09"^1 * P"/")
       * Ct(
                 Cg(Cc('none'),"unit")
               * Cg(R"09"^1,"year")  * Cg(Cc('year'),'unit')  * (P"/"
               * Cg(R"09"^1,"month") * Cg(Cc('month'),'unit') * (P"/"
               * Cg(R"09"^1,"day")   * Cg(Cc('day'),'unit')   * (
                       P"." * Cg(R"09"^1,'part') * Cg(Cc('part'),'unit')
                     + P"/" * Cg(R"!~"^1,'file') * Cg(Cc('file'),'unit')
                     )^-1)^-1)^-1
       ))
       / function(_,d)
           return format_unit[d.unit](d)
         end
       + P"file://localhost" -- second portion
         * P(blog.basedir)
         * P"/"
         * Ct(
               Cg(R"09"^1,'year')  * P"/" *
               Cg(R"09"^1,'month') * P"/" *
               Cg(R"09"^1,'day')   * P"/" *
               Cg(R"!~"^1,'file')  * Cg(Cc('file'),'unit')
             )
         / function(d)
             return format_unit[d.unit](d)
           end
       + affiliates(blog.affiliate)
       + P(1) -- last portion
   )^1)
end

-- ***********************************************************************

if _VERSION >= "Lua 5.2" then
 return _ENV
end