local Timer = require 'stylish.common.timer'
local Colors = require 'stylish.common.colors'
local Data = require 'stylish.common.data_rle'
local Canvas = require 'stylish.common.canvas'
local Utils = require 'stylish.common.util'
local Styles = require 'stylish.common.styles'
local Timer = require 'stylish.common.timer'

local api = vim.api

-- TODO: find a home for all these vars
local UPDATE_INTERVAL = 1000 / 60 -- animation frame rate
local ANIMATION_FADE_IN_TIME = 0.16
local ANIMATION_BRIGHTNESS_STEPS = 8 -- defines the number of gradient Colors created

local FADE_ENABLED = true
local SHOW_BACKGROUND = true

local BORDER_CHARS = Styles.border_chars
local DEFAULT_TEXT_COLOR = 'yellow'
-- local DEFAULT_TEXT_COLOR = "green"

local CLOCK_TEXT_COLOR = {
  ['green'] = '#829673',
  ['red'] = '#db0013',
  ['yellow'] = '#EFB009',
  ['border'] = '#9A8D7F',
}

-- TODO: ! These colors aren't generated by anything in this repo!
local TEXT_COLOR_PREFIX = ('Popup_%s_background'):format(DEFAULT_TEXT_COLOR)

local MASK_BLEND_LEVEL = 20

local FLOATWIN_WIDTH = 76
local FLOATWIN_HEIGHT = 7

-- TODO: move char_map to styles
local char_map = {
  [0] = ' ',
  [1] = '',
  [2] = '█',
  [3] = '',
}

-- TODO: fill_buffer should be a parameter on the window creation function
local filler = (' '):rep(FLOATWIN_WIDTH)
local FILLER_LINES = {}
for i = 1, FLOATWIN_HEIGHT do
  FILLER_LINES[i] = filler
end

local function textrow_to_chunks(row_values, row_len, hl_callback)
  local chunks = {}
  local chunk_virt_text, chunk_hl_group, cell_value, last_cell_value

  chunk_virt_text = ''
  for i = 1, #row_values + 1 do
    cell_value = row_values[i]

    if cell_value == last_cell_value then
      chunk_virt_text = chunk_virt_text .. char_map[cell_value]
    else
      if chunk_virt_text ~= '' then
        chunk_hl_group = hl_callback(last_cell_value)
        chunks[#chunks + 1] = { chunk_virt_text, chunk_hl_group }
      end

      last_cell_value = cell_value
      chunk_virt_text = char_map[cell_value]
    end
  end

  -- pad to row_len with zeros
  for _ = #row_values, row_len do
    chunks[#chunks + 1] = { ' ', hl_callback(0) }
  end

  return chunks
end

-- import font data
local function load_font()
  local file = 'clock_font.bin'
  local plugin_dir = vim.g.stylish_data_dir
  local filepath = ('%s%s'):format(plugin_dir, file)
  local font_data = Data.import_datafile(filepath)
  if not font_data then
    error('Error: unable to load logo data: ' .. filepath)
    return
  end
  return font_data
end

-- 'explodes' a string of numbers into a table of [0-9] values
local function str_decimal_explode(str)
  local res = {}
  for num in str:gmatch '%d' do
    res[#res + 1] = tonumber(num)
  end

  return res
end

--
local function create_line_indents()
  local res = {}
  for line = 1, 7 do -- TODO: replace hardcoded `7` with font height
    local row_indents = {}
    for i = 1, 4 + (FLOATWIN_HEIGHT - line) do
      row_indents[i] = 0
    end
    res[line] = row_indents
  end

  return res
end

-- TODO: make this a metamethod of the `clock_lines` table?
local function join_table_lines(table_a, table_b)
  for row, row_values in ipairs(table_b) do
    for _, val in ipairs(row_values) do
      table_a[row][#table_a[row] + 1] = val
    end
  end

  --   return table_a
end

-- blur background using box-blur algorithm
local function box_blur(grid_lines, animation_timer)
  local floor = math.floor
  local lerp = Utils.lerp

  local surrounding_cells = { { -1, 0 }, { -1, 1 }, { 0, 1 }, { 1, 1 }, { 1, 0 }, { 1, -1 }, { -1, -1 } }
  local elapsed_proportion = animation_timer and (animation_timer.elapsed / ANIMATION_FADE_IN_TIME) or 1
  -- print(elapsed_proportion)

  local blur_value, blur_average, blur_color_idx
  local blur_lines = {}
  for row = 2, #grid_lines - 1 do
    local blur_row = {}
    for col = 2, FLOATWIN_WIDTH - 1 do
      blur_average = 0
      for _, offset in ipairs(surrounding_cells) do
        blur_value = grid_lines[row + offset[1]][col + offset[2]] == 0 and 0 or 1
        blur_average = blur_average + blur_value
      end

      blur_average = blur_average / #surrounding_cells
      blur_color_idx = floor(lerp(12, 19, blur_average))

      -- taper left edge
      if col <= 2 then
        blur_color_idx = blur_color_idx + (3 - col)
      end
      -- annnd a bit off the top
      if row <= 4 then
        blur_color_idx = blur_color_idx + (5 - row)
      end

      blur_color_idx = (blur_color_idx > 19) and 19 or blur_color_idx -- limit:HACK

      -- fade-in animation
      if not (animation_timer == nil) then
        blur_color_idx = floor(lerp(19, blur_color_idx, elapsed_proportion))
      end

      blur_row[col] = blur_color_idx
    end

    blur_lines[#blur_lines + 1] = blur_row
  end

  return blur_lines
end

local function update_clock_display(context)
  local buf_set_extmark = vim.schedule_wrap(api.nvim_buf_set_extmark)
  local buf_add_hl = vim.schedule_wrap(api.nvim_buf_add_highlight)
  local buf_clear_namespace = vim.schedule_wrap(api.nvim_buf_clear_namespace)
  local lerp = Utils.lerp

  local floor = math.floor
  local date = os.date
  local update_delta_time = Timer.update_delta_time
  local explode = str_decimal_explode

  while true do
    local current_time = date '%H%M%S'
    -- don't update time matches already displayed, unless in animation phase
    if (current_time == context.last_displayed_time) and not context.animation_timer then
      -- print("YO!" .. os.clock())
      coroutine.yield()
      -- return
    end

    -- UPDATE DISPLAY

    context.last_displayed_time = current_time
    local time_tbl = explode(current_time)

    -- set brightness if fading in/out
    local brightness
    if context.animation_timer and FADE_ENABLED then
      update_delta_time(context.animation_timer)
      local elapsed
      if context.animation_timer.elapsed > ANIMATION_FADE_IN_TIME then
        elapsed = ANIMATION_FADE_IN_TIME
        -- clear completed timers
        -- TODO: close timer properly via class
        context.animation_timer = nil
      else
        elapsed = context.animation_timer.elapsed
      end
      brightness = floor(0.5 + lerp(1, ANIMATION_BRIGHTNESS_STEPS, elapsed / ANIMATION_FADE_IN_TIME))
    else
      brightness = ANIMATION_BRIGHTNESS_STEPS
    end
    -- print('brightness:' .. brightness)

    -- print(vim.inspect(context))
    --
    if not context.animation_timer then
      buf_clear_namespace(context.canvas.bufnr, context.canvas.nsid, 0, -1)
      -- print('clear: ' .. os.clock())
    end

    -- print(vim.inspect(clock_lines))

    local font_data = context.font_data
    -- insert line indents in numerical form
    local clock_lines = create_line_indents()

    -- join font character tables
    local font_size, font_idx, str_idx
    for i = 1, 5, 2 do
      font_size = (i <= 3) and 'large' or 'small'
      if i > 1 then
        join_table_lines(clock_lines, font_data.separator[font_size])
      end

      for j = 0, 1 do
        str_idx = i + j
        font_idx = time_tbl[str_idx] == 0 and 10 or time_tbl[str_idx]
        join_table_lines(clock_lines, font_data[font_size][font_idx])
      end
    end

    if SHOW_BACKGROUND then
      -- apply blur highlighting to floatwin background
      local blur_lines = box_blur(clock_lines, context.animation_timer)
      -- local blur_lines = clock_lines
      local blur_val, hl_group
      for row = 1, #blur_lines do
        for col = 1, FLOATWIN_WIDTH do
          blur_val = blur_lines[row][col]
          hl_group = blur_val and TEXT_COLOR_PREFIX .. blur_val or ''
          buf_add_hl(context.canvas.bufnr, context.canvas.nsid, hl_group, row, col - 1, col)
        end
      end
    end

    -- convert array of integers to mapped character extmark-chunks
    local row_chunks, opts
    for row, row_values in ipairs(clock_lines) do
      row_chunks = textrow_to_chunks(row_values, FLOATWIN_WIDTH, function(val)
        return (val == 0)
            and (((SHOW_BACKGROUND and ((row == 1) or (row == FLOATWIN_HEIGHT))) or not SHOW_BACKGROUND) and 'WidgetClockMask' or context.hl_groups[brightness])
          or context.hl_groups[brightness]
      end)

      opts = {
        id = context.clock_digit_extmarks[row],
        virt_text = row_chunks,
        virt_text_pos = 'overlay',
        hl_mode = 'combine',
      }
      buf_set_extmark(context.canvas.bufnr, context.canvas.nsid, row - 1, 0, opts)
    end

    --
    coroutine.yield()
  end
end

--

local function generate_colors(primary) end

local Clock = {}
Clock.__index = Clock

function Clock:new(user_config)
  local this = {}
  setmetatable(this, self)
  self.__index = self

  user_config = user_config or {}

  local color_hex_str = CLOCK_TEXT_COLOR.yellow
  Clock.hl_groups = Clock.hl_groups
    or Colors.create_gradient_map('WidgetClock', 'text', { color_hex_str }, ANIMATION_BRIGHTNESS_STEPS).fg[1]
  Clock.font_data = Clock.font_data or load_font()

  local padding = 2
  local statusline = 2
  -- TODO: allow for preset clock positions
  local parent_win_width = vim.o.columns
  local parent_win_height = vim.o.lines


  local editor_top = 1 -- TODO: change if tabline visible
  local editor_bot = parent_win_height - FLOATWIN_HEIGHT - padding - statusline - 1 -- TODO: change if statusline visible
  local editor_left = 0
  local editor_right = parent_win_width - FLOATWIN_WIDTH - 2

  local clock_positions = {
    top_left = {
      row = editor_top,
      col = editor_left,
    },
    top_right = {
      row = editor_top,
      col = editor_right,
    },
    bot_right = {
      row = editor_bot,
      col = editor_right,
    },
    bot_left = {
      row = editor_bot,
      col = editor_left
    }
  }

  local position = clock_positions.top_right
  local canvas_config = {
    win_opts = {
      row = position.row,
      col = position.col,
      height = FLOATWIN_HEIGHT,
      width = FLOATWIN_WIDTH,
      border = BORDER_CHARS,
    },
    focus_window = false,
    settings = {},
  }
  this.canvas = Canvas:new(canvas_config)

  -- fill the buffer with characters for extmarks to overlay
  api.nvim_buf_set_lines(this.canvas.bufnr, 0, FLOATWIN_HEIGHT - 1, false, FILLER_LINES)
  this.extmark_ids = Utils.extmark_create_batch(this.canvas.bufnr, this.canvas.nsid, FLOATWIN_HEIGHT)
  this.scene = coroutine.create(update_clock_display)
  this.clock_digit_extmarks = {}

  --   api.nvim_buf_clear_namespace(self.state.canvas.bufnr, self.state.nsid, 0, -1)
  --   print(vim.inspect(self.state))
  --   self.state.canvas:close()
  --   -- api.nvim_win_close(self.state.canvas.winid, true)
  --   self.state.canvas = nil
  -- end

  -- create highlight groups
  -- local user_color_valid = user_config.color and Colors.validate_hex_color(user_config.color)
  -- local color_hex_str = user_color_valid and user_config.color or TEXT_COLORS[DEFAULT_TEXT_COLOR]

  vim.cmd('hi! WidgetClockMask       guibg=background blend=' .. MASK_BLEND_LEVEL) -- NOTE: Masking blocks doesn't work at the same time as blur bg
  vim.cmd 'hi! WidgetClockBackground guibg=background blend=0'
  vim.cmd 'hi! WidgetClockBorder     guifg=#544E4A guibg=background'

  return this
end

function Clock:start()
  local update_interval = 1000
  self.animator = Timer:new(self.scene, self, update_interval)
  self.animator:start()
end

function Clock:stop()
  self.animator:stop()
  -- print(vim.inspect(self.canvas))
  self.canvas:close()
  self.canvas = nil -- TODO: what needs cleaning up?
end

return Clock
