local sundown = require 'sundown.env'
local C = sundown.C
local ffi = require 'ffi'
local bit

if _VERSION == 'Lua 5.2' or _VERSION == 'Lua 5.3' then
   bit = require 'bit32'
else
   bit = require 'bit'
end

require 'sundown.sdcdefs'

local c = {
   none = '\27[0m',
   black = '\27[0;30m',
   red = '\27[0;31m',
   green = '\27[0;32m',
   yellow = '\27[0;33m',
   blue = '\27[0;34m',
   magenta = '\27[0;35m',
   cyan = '\27[0;36m',
   white = '\27[0;37m',
   Black = '\27[1;30m',
   Red = '\27[1;31m',
   Green = '\27[1;32m',
   Yellow = '\27[1;33m',
   Blue = '\27[1;34m',
   Magenta = '\27[1;35m',
   Cyan = '\27[1;36m',
   White = '\27[1;37m',
   _black = '\27[40m',
   _red = '\27[41m',
   _green = '\27[42m',
   _yellow = '\27[43m',
   _blue = '\27[44m',
   _magenta = '\27[45m',
   _cyan = '\27[46m',
   _white = '\27[47m'
}

local color_style = {
   maxlsz = 80,
   none = c.none,
   h1 = c.Magenta,
   h2 = c.Red,
   h3 = c.Blue,
   h4 = c.Cyan,
   h5 = c.Green,
   h6 = c.Yellow,
   blockquote = '',
   hrule = c.Black,
   link = c.green,
   linkcontent = c.Green,
   code = c.cyan,
   emph = c.Black,
   doubleemph = c.Red,
   tripleemph = c.Magenta,
   strikethrough = c._white,
   header = c.White,
   footer = c.White,
   image = c.yellow,
   ulist = c.magenta,
   olist = c.magenta,
   tableheader = c.magenta,
   superscript = '^'
}

local bw_style = {
   maxlsz = 80,
   none = '',
   h1 = '',
   h2 = '',
   h3 = '',
   h4 = '',
   h5 = '',
   h6 = '',
   blockquote = '',
   hrule = '',
   link = '',
   linkcontent = '',
   code = '',
   emph = '',
   doubleemph = '',
   tripleemph = '',
   strikethrough = '',
   header = '',
   footer = '',
   image = '',
   ulist = '',
   olist = '',
   tableheader = '',
   superscript = '^'
}

local default_style = color_style

local function textsize(text)
   local szt = 0
   local nw = 0
   for word in text:gmatch('%S+') do
      local szw = #word
      word:gsub('\027%[[%d;]+m',
                function(stuff)
                   szw = szw - #stuff
                end)
      szt = szt + szw
      nw = nw+1
   end
   if nw > 0 then
      szt = szt + nw-1
   end
   return szt
end

local function createcallbacks(style)
   local tree = {}
   local n = 0

   local callbacks = {
      blockcode =
         function(ob, text, lang, opaque)
            if text ~= nil and text.data ~= nil then
               text = ffi.string(text.data, text.size)
               text = style.code .. text .. style.none
               n = n+1
               tree[n] = {tag='blockcode', text=text}
               C.sd_bufputs(ob, '\030' .. n .. '\031')
            end
         end,

      header =
         function(ob, text, level, opaque)
            if text ~= nil and text.data ~= nil then
               text = ffi.string(text.data, text.size)
               level = math.max(math.min(level, 6), 1)
               text = style['h' .. level] .. text .. style.none
               n = n+1
               tree[n] = {tag='header', text=text, level=level}
               C.sd_bufputs(ob, '\030' .. n .. '\031')
            end
         end,

      blockquote =
         function(ob, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = ffi.string(text.data, text.size)
               text = style.blockquote .. text .. style.none
               n = n+1
               tree[n] = {tag='blockquote', text=text}
               C.sd_bufputs(ob, '\030' .. n .. '\031')
            end
         end,

      blockhtml =
         function(ob, text, opaque)
            -- do nothing
         end,

      hrule =
         function(ob, opaque)
            n = n+1
            tree[n] = {tag='hrule'}
            C.sd_bufputs(ob, '\030' .. n .. '\031')
         end,

      paragraph =
         function(ob, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = ffi.string(text.data, text.size)
               n = n+1
               tree[n] = {tag='paragraph', text=text}
               C.sd_bufputs(ob, '\030' .. n .. '\031')
            end
         end,

      table =
         function(ob, header, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = ffi.string(text.data, text.size)
            else
               text = nil
            end

            if header ~= nil and header.data ~= nil then
               header = ffi.string(header.data, header.size)
            else
               header = nil
            end

            if text or header then
               n = n+1
               tree[n] = {tag='tbl', text=text, header=header}
               C.sd_bufputs(ob, '\030' .. n .. '\031')
            end
         end,

      table_row =
         function(ob, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = ffi.string(text.data, text.size)
               n = n+1
               tree[n] = {tag='tblrow', text=text}
               C.sd_bufputs(ob, '\030' .. n .. '\031')
            end
         end,

      table_cell =
         function(ob, text, flags, opaque)
            if text ~= nil and text.data ~= nil then
               text = ffi.string(text.data, text.size)
               if bit.band(flags, 4) > 0 then
                  text = style.tableheader .. text .. style.none
               end
               flags = bit.band(flags, 3)
               n = n+1
               tree[n] = {
                  tag='tblcell',
                  text=text,
                  size=textsize(text),
                  left=(flags==1),
                  right=(flags==2),
                  center=(flags==3)
               }
               C.sd_bufputs(ob, '\030' .. n .. '\031')
            end
         end,

      list =
         function(ob, text, flags, opaque)
            if text and text.data ~= nil then
               text = ffi.string(text.data, text.size)
               n = n+1
               tree[n] = {tag='list', text=text, type=bit.band(flags, 1)}
               C.sd_bufputs(ob, '\030' .. n .. '\031')
            end
         end,

      listitem =
         function(ob, text, flags, opaque)
            if text ~= nil and text.data ~= nil then
               text = ffi.string(text.data, text.size)
               n = n+1
               tree[n] = {tag='listitem', text=text}
               C.sd_bufputs(ob, '\030' .. n .. '\031')
            end
         end,

      normal_text =
         function(ob, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = ffi.string(text.data, text.size)
               text = text:gsub('[\029\030\031]', '')
               C.sd_bufputs(ob, text)
            end
         end,

      entity =
         function(ob, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = ffi.string(text.data, text.size)
               text = text:gsub('[\029\030\031]', '')
               C.sd_bufputs(ob, text)
            end
         end,

      autolink =
         function(ob, link, ltype, opaque)
            if link ~= nil and link.data ~= nil then
               link = ffi.string(link.data, link.size)
               link = style.link .. link .. style.none
               C.sd_bufputs(ob, link)
            end
            return 1
         end,

      codespan =
         function(ob, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = ffi.string(text.data, text.size)
               text = style.code .. text .. style.none
               C.sd_bufputs(ob, text)
            end
            return 1
         end,

      double_emphasis =
         function(ob, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = style.doubleemph .. ffi.string(text.data, text.size) .. style.none
               C.sd_bufputs(ob, text)
            end
            return 1
         end,

      emphasis =
         function(ob, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = style.emph .. ffi.string(text.data, text.size) .. style.none
               C.sd_bufputs(ob, text)
            end
            return 1
         end,

      image =
         function(ob, link, title, alt, opaque)
            local text = style.image .. '[image: '
            if title ~= nil and title.data ~= nil then
               text = text .. ffi.string(title.data, title.size)
            elseif alt ~= nil and alt.data ~= nil then
               text = text .. ffi.string(alt.data, alt.size)
            elseif link ~= nil and link.data ~= nil then
               text = text .. ffi.string(link.data, link.size)
            end
            text = text .. ']' .. style.none
            C.sd_bufputs(ob, text)
            return 1
         end,

      linebreak =
         function(ob, opaque)
            local text = '\029'
            C.sd_bufputs(ob, text)
         end,

      link =
         function(ob, link, title, content, opaque)
            local text = ''
            if content ~= nil and content.data ~= nil then
               text = style.linkcontent .. ffi.string(content.data, content.size) .. style.none
            end
            if link ~= nil and link.data ~= nil then
               local link = ffi.string(link.data, link.size)
               if not link:match('^#') then
                  text = text .. ' ' .. style.link .. '[' .. link .. ']' .. style.none
               end
            end
            if #text > 0 then
               C.sd_bufputs(ob, text)
            end
            return 1
         end,

      raw_html_tag =
         function(ob, tag, opaque)
            -- just ignore it
            return 1
         end,

      triple_emphasis =
         function(ob, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = style.tripleemph .. ffi.string(text.data, text.size) .. style.none
               C.sd_bufputs(ob, text)
            end
            return 1
         end,

      strikethrough =
         function(ob, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = style.strikethrough .. ffi.string(text.data, text.size) .. style.none
               C.sd_bufputs(ob, text)
            end
            return 1
         end,

      superscript =
         function(ob, text, opaque)
            if text ~= nil and text.data ~= nil then
               text = style.superscript .. ffi.string(text.data, text.size) .. style.none
               C.sd_bufputs(ob, text)
            end
            return 1
         end,

      doc_header =
         function(ob, opaque)
         end,

      doc_footer =
         function(ob, opaque)
         end
   }

   return callbacks, tree
end


local function preprocess(txt, style)
   local callbacks, tree = createcallbacks(style)
   local c_callbacks = ffi.new('struct sd_callbacks', callbacks)
   local options = nil
   local markdown = C.sd_markdown_new(0xfff, 16, c_callbacks, options)

   local outbuf = C.sd_bufnew(64)

   C.sd_markdown_render(outbuf, ffi.cast('const char*', txt), #txt, markdown)
   C.sd_markdown_free(markdown)

   for name,_ in pairs(callbacks) do
      c_callbacks[name]:free()
   end

   txt = ffi.string(outbuf.data, outbuf.size)
   C.sd_bufrelease(outbuf)

   return txt, tree
end

local function showindent(out, text, indent)
   for line, brk in text:gmatch('([^\n]+)([\n]*)') do
      table.insert(out, string.rep(' ', indent) .. line)
      for i=1,#brk-1 do
         table.insert(out, '')
      end
   end
end

local function showjustified(out, text, indent, maxlsz)
   local lines = {}
   local szl = 0
   local line = {}

   local function newline()
      if #line > 0 then
         table.insert(lines, string.rep(' ', indent) .. table.concat(line, ' '))
         line = {}
         szl = 0
      end
   end

   for word in text:gmatch('%S+') do
      local szw = #word
      word:gsub('\027%[[%d;]+m',
                function(stuff)
                   szw = szw - #stuff
                end)

      if szl+szw+1 > maxlsz-indent then
         newline()
      end
      table.insert(line, word)
      szl = szl + szw+1
   end
   newline()

   table.insert(out, table.concat(lines, '\n'))
end

local function show(out, txt, tree, indent, style, maxlsz)
   maxlsz = maxlsz or style.maxlsz

   local idx = 1
   local node

   while true do
      local i, j
      i, j = txt:find('[^\029\030\031]+', idx)
      if i and i == idx then
         showjustified(out, txt:sub(i, j), indent, maxlsz)
         idx = j+1
      else
         i, j = txt:find('\029', idx)
         if i and i == idx then
            table.insert(out, '')
            idx = j+1
         else
            i, j = txt:find('(%b\030\031)', idx)
            if i and i == idx then
               idx = j+1
               local node = tree[ tonumber(txt:sub(i+1, j-1)) ]
               if node.tag == 'blockcode' then
                  table.insert(out, '')
                  showindent(out, node.text, indent)
               elseif node.tag == 'blockquote' then
                  table.insert(out, '')
                  show(out, node.text, tree, indent+5, style, maxlsz-5)
               elseif node.tag == 'header' then
                  table.insert(out, '')
                  indent = node.level
                  showindent(out, style['h' .. node.level] .. string.rep('+', maxlsz-indent+1) .. style.none, indent-1)
                  showjustified(out, node.text, indent-1, maxlsz)
               elseif node.tag == 'hrule' then
                  table.insert(out, '')
                  showindent(out, style.hrule .. string.rep('_', maxlsz-indent) .. style.none, indent)
               elseif node.tag == 'paragraph' then
                  table.insert(out, '')
                  showjustified(out, node.text, indent, maxlsz, style)
               elseif node.tag == 'list' then
                  if node.type == 0 then
                     for nidx in node.text:gmatch('(%b\030\031)') do
                        local subnode = tree[ tonumber(nidx:sub(2, -2)) ]
                        while subnode.text:match('^(%b\030\031)') do
                           subnode = tree[ tonumber( subnode.text:match('^(%b\030\031)'):sub(2, -2) ) ]
                        end
                        subnode.text = style.ulist .. '* ' .. style.none .. subnode.text
                     end
                  else
                     local oidx = 0
                     for nidx in node.text:gmatch('(%b\030\031)') do
                        local subnode = tree[ tonumber(nidx:sub(2, -2)) ]
                        while subnode.text:match('^(%b\030\031)') do
                           subnode = tree[ tonumber( subnode.text:match('^(%b\030\031)'):sub(2, -2) ) ]
                        end
                        oidx = oidx + 1
                        subnode.text = style.olist .. oidx .. '. ' .. style.none .. subnode.text
                     end
                  end
                  table.insert(out, '')
                  show(out, node.text, tree, indent+3, style, maxlsz)
                  table.insert(out, '')
               elseif node.tag == 'listitem' then
                  show(out, node.text, tree, indent, style, maxlsz)
               elseif node.tag == 'tbl' then
                  -- find cell sizes
                  local function rendertblsz(text, maxsz)
                     local idxrow = 0
                     for row in text:gmatch('(%b\030\031)') do
                        idxrow = idxrow + 1
                        local sz = {}
                        row = tree[ tonumber(row:sub(2, -2)) ]
                        assert(row.tag == 'tblrow')
                        local idxcell = 0
                        for cell in row.text:gmatch('(%b\030\031)') do
                           idxcell = idxcell + 1
                           sz[idxcell] = sz[idxcell] or 0
                           maxsz[idxcell] = maxsz[idxcell] or 0
                           cell = tree[ tonumber(cell:sub(2, -2)) ]
                           assert(cell.tag == 'tblcell')
                           sz[idxcell] = sz[idxcell] + cell.size
                        end
                        for idxcell=1,#sz do
                           maxsz[idxcell] = math.max(maxsz[idxcell], sz[idxcell])
                        end
                     end
                  end

                  local maxsz = {}
                  rendertblsz(node.header, maxsz)
                  rendertblsz(node.text, maxsz)

                  -- print it
                  local function rendertbl(text, maxsz, isheader)
                     local sztot = 0
                     for i=1,#maxsz do
                        sztot = sztot + maxsz[i]
                     end
                     local idxrow = 0
                     if isheader then
                        showindent(out, ' ' .. string.rep('-', sztot+(#maxsz-1)*3+2), indent)
                     end
                     for row in text:gmatch('(%b\030\031)') do
                        idxrow = idxrow + 1
                        row = tree[ tonumber(row:sub(2, -2)) ]
                        local line = {}
                        local idxcell = 0
                        for cell in row.text:gmatch('(%b\030\031)') do
                           idxcell = idxcell + 1
                           cell = tree[ tonumber(cell:sub(2, -2)) ]
                           if cell.right then
                              table.insert(line, string.rep(' ', maxsz[idxcell]-cell.size) .. cell.text)
                           elseif cell.center then
                              local szh2 = math.floor((maxsz[idxcell]-cell.size)/2)
                              table.insert(line, string.rep(' ', szh2) .. cell.text .. string.rep(' ', maxsz[idxcell]-cell.size-szh2))
                           else
                              table.insert(line, cell.text .. string.rep(' ', maxsz[idxcell]-cell.size))
                           end
                        end
                        showindent(out, '| ' .. table.concat(line, ' | ') .. ' |', indent)
                     end
                     showindent(out, ' ' .. string.rep('-', sztot+(#maxsz-1)*3+2), indent)
                  end
                  rendertbl(node.header, maxsz, true)
                  rendertbl(node.text, maxsz)
               end
            else
               break
            end
         end
      end
   end
end

local function render(txt, style)
   local tree
   local out = {}

   style = style or default_style
   txt, tree = preprocess(txt, style)

   show(out, txt, tree, 0, style)

   return table.concat(out, '\n')
end

local function color()
   default_style = color_style
   return default_style
end

local function bw()
   default_style = bw_style
   return default_style
end

return {render=render, bw=bw, color=color}
