local mp = require 'mp'
local utils = require 'mp.utils'
local assdraw = require 'mp.assdraw'

-- create namespace with default values
local em = {

  -- customisable values ------------------------------------------------------

  lines_to_show = 17, -- NOT including search line
  pause_on_open = true,
  resume_on_exit = "only-if-was-paused", -- another possible value is true

  -- styles (earlyer it was a table, but required many more steps to pass def-s
  --            here from .conf file)
  font_size = 21,
  -- cursor 'width', useful to change if you have hidpi monitor
  cursor_x_border = 0.3,
  line_bottom_margin = 1, -- basically space between lines
  text_color = {
    default = 'ffffff',
    accent = 'd8a07b',
    current = 'aaaaaa',
    comment = '636363',
  },
  menu_x_padding = 5, -- this padding for now applies only to 'left', not x
  menu_y_padding = 2, -- but this one applies to both - top & bottom


  -- values that should be passed from main script ----------------------------

  search_heading = 'Default search heading',
  -- 'full' is required from main script, 'current_i' is optional
  -- others are 'private'
  list = {
    full = {}, filtered = {}, current_i = nil, pointer_i = 1, show_from_to = {}
  },
  -- field to compare with when searching for 'current value' by 'current_i'
  index_field = 'index',
  -- fields to use when searching for string match / any other custom searching
  -- if value has 0 length, then search list item itself
  filter_by_fields = {},


  -- 'private' values that are not supposed to be changed from the outside ----

  is_active = false,
  -- https://mpv.io/manual/master/#lua-scripting-mp-create-osd-overlay(format)
  ass = mp.create_osd_overlay("ass-events"),
  was_paused = false, -- flag that indicates that vid was paused by this script

  line = '',
  -- if there was no cursor it wouldn't have been needed, but for now we need
  -- variable below only to compare it with 'line' and see if we need to filter
  prev_line = '',
  cursor = 1,
  history = {},
  history_pos = 1,
  key_bindings = {},
  insert_mode = false,

  -- used only in 'update' func to get error text msgs
  error_codes = {
    no_match = 'Match required',
    no_submit_provided = 'No submit function provided'
  }
}

-- PRIVATE METHODS ------------------------------------------------------------

-- declare constructor function
function em:new(o)
  o = o or {}
  setmetatable(o, self)
  self.__index = self

  -- some options might be customised by user in .conf file and read as strings
  -- in that case parse those
  if type(o.filter_by_fields) == 'string' then
    o.filter_by_fields = utils.parse_json(o.filter_by_fields)
  end

  if type(o.text_color) == 'string' then
    o.text_color = utils.parse_json(o.text_color)
  end

  return o
end

-- this func is just a getter of a current list depending on search line
function em:current()
  return self.line == '' and self.list.full or self.list.filtered
end

-- REVIEW: how to get rid of this wrapper and handle filter func sideeffects
-- in a more elegant way?
function em:filter_wrapper()
  -- handles sideeffect that are needed to be run on filtering list
  -- cuz the filter func may be redefined in main script and therefore needs
  -- to be straight forward - only doing filtering and returning the table

  -- passing current query just in case, so ppl can use it in their custom funcs
  self.list.filtered = self:filter(self.line)

  self.prev_line = self.line
  self.list.pointer_i = 1
  self:set_from_to(true)
end

function em:set_from_to(reset_flag)
  -- additional variables just for shorter var name
  local i = self.list.pointer_i
  local to_show = self.lines_to_show
  local total = #self:current()

  if reset_flag or to_show >= total then
    self.list.show_from_to = { 1, math.min(to_show, total) }
    return
  end

  -- If menu is opened with something already selected we want this 'selected'
  -- to be displayed close to the middle of the menu. That's why 'show_from_to'
  -- is not initially set, so we can know - if show_from_to length is 0 - it is
  -- first call of this func in cur. init
  if #self.list.show_from_to == 0 then
    -- set show_from_to so chosen item will be displayed close to middle
    local half_list = math.ceil(to_show / 2)
    if i < half_list then
      self.list.show_from_to = { 1, to_show }
    elseif total - i < half_list then
      self.list.show_from_to = { total - to_show + 1, total }
    else
      self.list.show_from_to = { i - half_list + 1, i - half_list + to_show }
    end
  else
    local first, last = table.unpack(self.list.show_from_to)

    -- handle cursor moving towards start / end bondary
    if first ~= 1 and i - first < 2 then
      self.list.show_from_to = { first - 1, last - 1 }
    end
    if last ~= total and last - i < 2 then
      self.list.show_from_to = { first + 1, last + 1 }
    end

    -- handle index jumps from beginning to end and backwards
    if i > last then
      self.list.show_from_to = { i - to_show + 1, i }
    end
    if i < first then self.list.show_from_to = { 1, to_show } end
  end
end

function em:change_selected_index(num)
  self.list.pointer_i = self.list.pointer_i + num
  if self.list.pointer_i < 1 then
    self.list.pointer_i = #self:current()
  elseif self.list.pointer_i > #self:current() then
    self.list.pointer_i = 1
  end
  self:set_from_to()
  self:update()
end

-- Render the REPL and console as an ASS OSD
function em:update(err_code)
  -- ASS tags documentation here - https://aegi.vmoe.info/docs/3.0/ASS_Tags/

  -- do not bother if function was called to close the menu..
  if not self.is_active then
    em.ass:remove()
    return
  end

  local line_height = self.font_size + self.line_bottom_margin
  local ww, wh = mp.get_osd_size() -- window width & height
  -- '+ 1' below is a search string
  local menu_y_pos =
  wh - (line_height * (self.lines_to_show + 1) + self.menu_y_padding * 2)

  -- didn't find better place to handle filtered list update
  if self.line ~= self.prev_line then self:filter_wrapper() end

  local function get_background()
    local a = self:ass_new_wrapper()
    a:append('{\\1c&H1c1c1c\\1a&H19}') -- background color & opacity
    a:pos(0, 0)
    a:draw_start()
    a:rect_cw(0, menu_y_pos, ww, wh)
    a:draw_stop()
    return a.text
  end

  local function get_search_header()
    local a = self:ass_new_wrapper()

    a:pos(self.menu_x_padding, menu_y_pos + self.menu_y_padding)

    local search_prefix = table.concat({
      self:get_font_color('accent'),
      (#self:current() ~= 0 and self.list.pointer_i or '!'),
      '/', #self:current(), '\\h\\h', self.search_heading, ':\\h'
    });

    a:append(search_prefix)
    -- reset font color after search prefix
    a:append(self:get_font_color 'default')

    -- Create the cursor glyph as an ASS drawing. ASS will draw the cursor
    -- inline with the surrounding text, but it sets the advance to the width
    -- of the drawing. So the cursor doesn't affect layout too much, make it as
    -- thin as possible and make it appear to be 1px wide by giving it 0.5px
    -- horizontal borders.
    local cheight = self.font_size * 8
    -- TODO: maybe do it using draw_rect from ass?
    local cglyph = '{\\r' .. -- styles reset
        '\\1c&Hffffff&\\3c&Hffffff' .. -- font color and border color
        '\\xbord' .. self.cursor_x_border .. '\\p4\\pbo24}' .. -- xborder, scale x8 and baseline offset
        'm 0 0 l 0 ' .. cheight .. -- drawing just a line
        '{\\p0\\r}' -- finish drawing and reset styles
    local before_cur = self:ass_escape(self.line:sub(1, self.cursor - 1))
    local after_cur = self:ass_escape(self.line:sub(self.cursor))

    a:append(table.concat({
      before_cur, cglyph, self:reset_styles(),
      self:get_font_color('default'), after_cur,
      (err_code and '\\h' .. self.error_codes[err_code] or "")
    }))

    return a.text

    -- NOTE: perhaps this commented code will some day help me in coding cursor
    -- like in M-x emacs menu:
    -- Redraw the cursor with the REPL text invisible. This will make the
    -- cursor appear in front of the text.
    -- ass:new_event()
    -- ass:an(1)
    -- ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur)
    -- ass:append(cglyph)
    -- ass:append(style .. '{\\alpha&HFF&}' .. after_cur)
  end

  local function get_list()
    local a = assdraw.ass_new()

    local function apply_highlighting(y)
      a:new_event()
      a:append(self:reset_styles())
      a:append('{\\1c&Hffffff\\1a&HE6}') -- background color & opacity
      a:pos(0, 0)
      a:draw_start()
      a:rect_cw(0, y, ww, y + self.font_size)
      a:draw_stop()
    end

    -- REVIEW: maybe make another function 'get_line_str' and move there
    -- everything from this for loop?
    -- REVIEW: how to use something like table.unpack below?
    for i = self.list.show_from_to[1], self.list.show_from_to[2] do
      local value = assert(self:current()[i], 'no value with index ' .. i)
      local y_offset = menu_y_pos + self.menu_y_padding +
          (line_height * (i - self.list.show_from_to[1] + 1))

      if i == self.list.pointer_i then apply_highlighting(y_offset) end

      a:new_event()
      a:append(self:reset_styles())
      a:pos(self.menu_x_padding, y_offset)
      a:append(self:get_line(i, value))
    end

    return a.text
  end

  em.ass.res_x = ww
  em.ass.res_y = wh
  em.ass.data = table.concat({
    get_background(),
    get_search_header(),
    get_list()
  }, "\n")

  em.ass:update()

end

-- params:
--  - data : {list: {}, [current_i] : num}
function em:init(data)
  self.list.full = data.list or {}
  self.list.current_i = data.current_i or nil
  self.list.pointer_i = data.current_i or 1
  self:set_active(true)
end

function em:exit()
  self:undefine_key_bindings()
  collectgarbage()
end

-- TODO: write some idle func like this
-- function idle()
--     if pending_selection then
--         gallery:set_selection(pending_selection)
--         pending_selection = nil
--     end
--     if ass_changed or geometry_changed then
--         local ww, wh = mp.get_osd_size()
--         if geometry_changed then
--             geometry_changed = false
--             compute_geometry(ww, wh)
--         end
--         if ass_changed then
--             ass_changed = false
--             mp.set_osd_ass(ww, wh, ass)
--         end
--     end
-- end
-- ...
-- and handle it as follows
-- init():
-- mp.register_idle(idle)
-- idle()
-- exit():
-- mp.unregister_idle(idle)
-- idle()
-- And in these observers he is setting a flag, that's being checked in func above
-- mp.observe_property("osd-width", "native", mark_geometry_stale)
-- mp.observe_property("osd-height", "native", mark_geometry_stale)

-- PRIVATE METHODS END --------------------------------------------------------

-- PUBLIC METHODS -------------------------------------------------------------

function em:filter()
  -- default filter func, might be redefined in main script
  local result = {}

  local function get_full_search_str(v)
    local str = ''
    for _, key in ipairs(self.filter_by_fields) do str = str .. (v[key] or '') end
    return str
  end

  for _, v in ipairs(self.list.full) do
    -- if filter_by_fields has 0 length, then search list item itself
    if #self.filter_by_fields == 0 then
      if self:search_method(v) then table.insert(result, v) end
    else
      -- NOTE: we might use search_method on fiels separately like this:
      -- for _,key in ipairs(self.filter_by_fields) do
      --   if self:search_method(v[key]) then table.insert(result, v) end
      -- end
      -- But since im planning to implement fuzzy search in future i need full
      -- search string here
      if self:search_method(get_full_search_str(v)) then
        table.insert(result, v)
      end
    end
  end
  return result
end

-- TODO: implement fuzzy search and maybe match highlights
function em:search_method(str)
  -- also might be redefined by main script

  -- convert to string just to make sure..
  return tostring(str):lower():find(self.line:lower(), 1, true)
end

-- this module requires submit function to be defined in main script
function em:submit() self:update('no_submit_provided') end

function em:update_list(list)
  -- for now this func doesn't handle cases when we have 'current_i' to update
  -- it
  self.list.full = list
  if self.line ~= self.prev_line then self:filter_wrapper() end
end

-- PUBLIC METHODS END ---------------------------------------------------------

-- HELPER METHODS -------------------------------------------------------------

function em:get_line(_, v) -- [i]ndex, [v]alue
  -- this func might be redefined in main script to get a custom-formatted line
  -- default implementation of this func supposes that value.content field is a
  -- String
  local a = assdraw.ass_new()
  local style = (self.list.current_i == v[self.index_field])
      and 'current' or 'default'

  a:append(self:reset_styles())
  a:append(self:get_font_color(style))
  -- content as default field, which is holding string
  -- no point in moving it to main object since content itself is being
  -- composed in THIS function, that might (and most likely, should) be
  -- redefined in main script
  a:append(v.content or 'Something is off in `get_line` func')
  return a.text
end

-- REVIEW: for now i don't see normal way of mergin this func with below one
-- but it's being used only once
function em:reset_styles()
  local a = assdraw.ass_new()
  -- alignment top left, no word wrapping, border 0, shadow 0
  a:append('{\\an7\\q2\\bord0\\shad0}')
  a:append('{\\fs' .. self.font_size .. '}')
  return a.text
end

-- function to get rid of some copypaste
function em:ass_new_wrapper()
  local a = assdraw.ass_new()
  a:new_event()
  a:append(self:reset_styles())
  return a
end

function em:get_font_color(style)
  return '{\\1c&H' .. self.text_color[style] .. '}'
end

-- HELPER METHODS END ---------------------------------------------------------


--[[
  The below code is a modified implementation of text input from mpv's console.lua:
  https://github.com/mpv-player/mpv/blob/87c9eefb2928252497f6141e847b74ad1158bc61/player/lua/console.lua

  I was too lazy to list all modifications i've done to the script, but if u
  rly need to see those - do diff with the original code
]] --

-------------------------------------------------------------------------------
--                          START ORIGINAL MPV CODE                          --
-------------------------------------------------------------------------------

-- Copyright (C) 2019 the mpv developers
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

function em:detect_platform()
  local o = {}
  -- Kind of a dumb way of detecting the platform but whatever
  if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then
    return 'windows'
  elseif mp.get_property_native('options/macos-force-dedicated-gpu', o) ~= o then
    return 'macos'
  elseif os.getenv('WAYLAND_DISPLAY') then
    return 'wayland'
  end
  return 'x11'
end

-- Escape a string for verbatim display on the OSD
function em:ass_escape(str)
  -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if
  -- it isn't followed by a recognised character, so add a zero-width
  -- non-breaking space
  str = str:gsub('\\', '\\\239\187\191')
  str = str:gsub('{', '\\{')
  str = str:gsub('}', '\\}')
  -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of
  -- consecutive newlines
  str = str:gsub('\n', '\239\187\191\\N')
  -- Turn leading spaces into hard spaces to prevent ASS from stripping them
  str = str:gsub('\\N ', '\\N\\h')
  str = str:gsub('^ ', '\\h')
  return str
end

-- Set the REPL visibility ("enable", Esc)
function em:set_active(active)
  if active == self.is_active then return end
  if active then
    self.is_active = true
    self.insert_mode = false
    mp.enable_messages('terminal-default')
    self:define_key_bindings()

    -- set flag 'was_paused' only if vid wasn't paused before EM init
    if self.pause_on_open and not mp.get_property_bool("pause", false) then
      mp.set_property_bool("pause", true)
      self.was_paused = true
    end

    self:set_from_to()
    self:update()
  else
    -- no need to call 'update' in this block cuz 'clear' method is calling it
    self.is_active = false
    self:undefine_key_bindings()

    if self.resume_on_exit == true or
        (self.resume_on_exit == "only-if-was-paused" and self.was_paused) then
      mp.set_property_bool("pause", false)
    end

    self:clear()
    collectgarbage()
  end
end

-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
function em:next_utf8(str, pos)
  if pos > str:len() then return pos end
  repeat
    pos = pos + 1
  until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
  return pos
end

-- As above, but finds the previous UTF-8 charcter in 'str' before 'pos'
function em:prev_utf8(str, pos)
  if pos <= 1 then return pos end
  repeat
    pos = pos - 1
  until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
  return pos
end

-- Insert a character at the current cursor position (any_unicode)
function em:handle_char_input(c)
  if self.insert_mode then
    self.line = self.line:sub(1, self.cursor - 1) .. c .. self.line:sub(self:next_utf8(self.line, self.cursor))
  else
    self.line = self.line:sub(1, self.cursor - 1) .. c .. self.line:sub(self.cursor)
  end
  self.cursor = self.cursor + #c
  self:update()
end

-- Remove the character behind the cursor (Backspace)
function em:handle_backspace()
  if self.cursor <= 1 then return end
  local prev = self:prev_utf8(self.line, self.cursor)
  self.line = self.line:sub(1, prev - 1) .. self.line:sub(self.cursor)
  self.cursor = prev
  self:update()
end

-- Remove the character in front of the cursor (Del)
function em:handle_del()
  if self.cursor > self.line:len() then return end
  self.line = self.line:sub(1, self.cursor - 1) .. self.line:sub(self:next_utf8(self.line, self.cursor))
  self:update()
end

-- Toggle insert mode (Ins)
function em:handle_ins()
  self.insert_mode = not self.insert_mode
end

-- Move the cursor to the next character (Right)
function em:next_char()
  self.cursor = self:next_utf8(self.line, self.cursor)
  self:update()
end

-- Move the cursor to the previous character (Left)
function em:prev_char()
  self.cursor = self:prev_utf8(self.line, self.cursor)
  self:update()
end

-- Clear the current line (Ctrl+C)
function em:clear()
  self.line = ''
  self.prev_line = ''

  self.list.current_i = nil
  self.list.pointer_i = 1
  self.list.filtered = {}
  self.list.show_from_to = {}

  self.was_paused = false

  self.cursor = 1
  self.insert_mode = false
  self.history_pos = #self.history + 1

  self:update()
end

-- Run the current command and clear the line (Enter)
function em:handle_enter()
  if #self:current() == 0 then
    self:update('no_match')
    return
  end

  if self.history[#self.history] ~= self.line then
    self.history[#self.history + 1] = self.line
  end

  self:submit(self:current()[self.list.pointer_i])
  self:set_active(false)
end

-- Go to the specified position in the command history
function em:go_history(new_pos)
  local old_pos = self.history_pos
  self.history_pos = new_pos

  -- Restrict the position to a legal value
  if self.history_pos > #self.history + 1 then
    self.history_pos = #self.history + 1
  elseif self.history_pos < 1 then
    self.history_pos = 1
  end

  -- Do nothing if the history position didn't actually change
  if self.history_pos == old_pos then
    return
  end

  -- If the user was editing a non-history line, save it as the last history
  -- entry. This makes it much less frustrating to accidentally hit Up/Down
  -- while editing a line.
  if old_pos == #self.history + 1 and self.line ~= '' and self.history[#self.history] ~= self.line then
    self.history[#self.history + 1] = self.line
  end

  -- Now show the history line (or a blank line for #history + 1)
  if self.history_pos <= #self.history then
    self.line = self.history[self.history_pos]
  else
    self.line = ''
  end
  self.cursor = self.line:len() + 1
  self.insert_mode = false
  self:update()
end

-- Go to the specified relative position in the command history (Up, Down)
function em:move_history(amount)
  self:go_history(self.history_pos + amount)
end

-- Go to the first command in the command history (PgUp)
function em:handle_pgup()
  self:go_history(1)
end

-- Stop browsing history and start editing a blank line (PgDown)
function em:handle_pgdown()
  self:go_history(#self.history + 1)
end

-- Move to the start of the current word, or if already at the start, the start
-- of the previous word. (Ctrl+Left)
function em:prev_word()
  -- This is basically the same as next_word() but backwards, so reverse the
  -- string in order to do a "backwards" find. This wouldn't be as annoying
  -- to do if Lua didn't insist on 1-based indexing.
  self.cursor = self.line:len() - select(2, self.line:reverse():find('%s*[^%s]*', self.line:len() - self.cursor + 2)) + 1
  self:update()
end

-- Move to the end of the current word, or if already at the end, the end of
-- the next word. (Ctrl+Right)
function em:next_word()
  self.cursor = select(2, self.line:find('%s*[^%s]*', self.cursor)) + 1
  self:update()
end

-- Move the cursor to the beginning of the line (HOME)
function em:go_home()
  self.cursor = 1
  self:update()
end

-- Move the cursor to the end of the line (END)
function em:go_end()
  self.cursor = self.line:len() + 1
  self:update()
end

-- Delete from the cursor to the beginning of the word (Ctrl+Backspace)
function em:del_word()
  local before_cur = self.line:sub(1, self.cursor - 1)
  local after_cur = self.line:sub(self.cursor)

  before_cur = before_cur:gsub('[^%s]+%s*$', '', 1)
  self.line = before_cur .. after_cur
  self.cursor = before_cur:len() + 1
  self:update()
end

-- Delete from the cursor to the end of the word (Ctrl+Del)
function em:del_next_word()
  if self.cursor > self.line:len() then return end

  local before_cur = self.line:sub(1, self.cursor - 1)
  local after_cur = self.line:sub(self.cursor)

  after_cur = after_cur:gsub('^%s*[^%s]+', '', 1)
  self.line = before_cur .. after_cur
  self:update()
end

-- Delete from the cursor to the end of the line (Ctrl+K)
function em:del_to_eol()
  self.line = self.line:sub(1, self.cursor - 1)
  self:update()
end

-- Delete from the cursor back to the start of the line (Ctrl+U)
function em:del_to_start()
  self.line = self.line:sub(self.cursor)
  self.cursor = 1
  self:update()
end

-- Returns a string of UTF-8 text from the clipboard (or the primary selection)
function em:get_clipboard(clip)
  -- Pick a better default font for Windows and macOS
  local platform = self:detect_platform()

  if platform == 'x11' then
    local res = utils.subprocess({
      args = { 'xclip', '-selection', clip and 'clipboard' or 'primary', '-out' },
      playback_only = false,
    })
    if not res.error then
      return res.stdout
    end
  elseif platform == 'wayland' then
    local res = utils.subprocess({
      args = { 'wl-paste', clip and '-n' or '-np' },
      playback_only = false,
    })
    if not res.error then
      return res.stdout
    end
  elseif platform == 'windows' then
    local res = utils.subprocess({
      args = { 'powershell', '-NoProfile', '-Command', [[& {
                Trap {
                    Write-Error -ErrorRecord $_
                    Exit 1
                }

                $clip = ""
                if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) {
                    $clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
                } else {
                    Add-Type -AssemblyName PresentationCore
                    $clip = [Windows.Clipboard]::GetText()
                }

                $clip = $clip -Replace "`r",""
                $u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
                [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
            }]] },
      playback_only = false,
    })
    if not res.error then
      return res.stdout
    end
  elseif platform == 'macos' then
    local res = utils.subprocess({
      args = { 'pbpaste' },
      playback_only = false,
    })
    if not res.error then
      return res.stdout
    end
  end
  return ''
end

-- Paste text from the window-system's clipboard. 'clip' determines whether the
-- clipboard or the primary selection buffer is used (on X11 and Wayland only.)
function em:paste(clip)
  local text = self:get_clipboard(clip)
  local before_cur = self.line:sub(1, self.cursor - 1)
  local after_cur = self.line:sub(self.cursor)
  self.line = before_cur .. text .. after_cur
  self.cursor = self.cursor + text:len()
  self:update()
end

-- List of input bindings. This is a weird mashup between common GUI text-input
-- bindings and readline bindings.
function em:get_bindings()
  local bindings = {
    { 'ctrl+[', function() self:set_active(false) end },
    { 'ctrl+g', function() self:set_active(false) end },
    { 'esc', function() self:set_active(false) end },
    { 'enter', function() self:handle_enter() end },
    { 'kp_enter', function() self:handle_enter() end },
    { 'ctrl+m', function() self:handle_enter() end },
    { 'bs', function() self:handle_backspace() end },
    { 'shift+bs', function() self:handle_backspace() end },
    { 'ctrl+h', function() self:handle_backspace() end },
    { 'del', function() self:handle_del() end },
    { 'shift+del', function() self:handle_del() end },
    { 'ins', function() self:handle_ins() end },
    { 'shift+ins', function() self:paste(false) end },
    { 'mbtn_mid', function() self:paste(false) end },
    { 'left', function() self:prev_char() end },
    { 'ctrl+b', function() self:prev_char() end },
    { 'right', function() self:next_char() end },
    { 'ctrl+f', function() self:next_char() end },
    { 'ctrl+k', function() self:change_selected_index(-1) end },
    { 'ctrl+p', function() self:change_selected_index(-1) end },
    { 'ctrl+j', function() self:change_selected_index(1) end },
    { 'ctrl+n', function() self:change_selected_index(1) end },
    { 'up', function() self:move_history(-1) end },
    { 'alt+p', function() self:move_history(-1) end },
    { 'wheel_up', function() self:move_history(-1) end },
    { 'down', function() self:move_history(1) end },
    { 'alt+n', function() self:move_history(1) end },
    { 'wheel_down', function() self:move_history(1) end },
    { 'wheel_left', function() end },
    { 'wheel_right', function() end },
    { 'ctrl+left', function() self:prev_word() end },
    { 'alt+b', function() self:prev_word() end },
    { 'ctrl+right', function() self:next_word() end },
    { 'alt+f', function() self:next_word() end },
    { 'ctrl+a', function() self:go_home() end },
    { 'home', function() self:go_home() end },
    { 'ctrl+e', function() self:go_end() end },
    { 'end', function() self:go_end() end },
    { 'pgup', function() self:handle_pgup() end },
    { 'pgdwn', function() self:handle_pgdown() end },
    { 'ctrl+c', function() self:clear() end },
    { 'ctrl+d', function() self:handle_del() end },
    { 'ctrl+u', function() self:del_to_start() end },
    { 'ctrl+v', function() self:paste(true) end },
    { 'meta+v', function() self:paste(true) end },
    { 'ctrl+bs', function() self:del_word() end },
    { 'ctrl+w', function() self:del_word() end },
    { 'ctrl+del', function() self:del_next_word() end },
    { 'alt+d', function() self:del_next_word() end },
    { 'kp_dec', function() self:handle_char_input('.') end },
  }

  for i = 0, 9 do
    bindings[#bindings + 1] =
    { 'kp' .. i, function() self:handle_char_input('' .. i) end }
  end

  return bindings
end

function em:text_input(info)
  if info.key_text and (info.event == "press" or info.event == "down"
      or info.event == "repeat")
  then
    self:handle_char_input(info.key_text)
  end
end

function em:define_key_bindings()
  if #self.key_bindings > 0 then
    return
  end
  for _, bind in ipairs(self:get_bindings()) do
    -- Generate arbitrary name for removing the bindings later.
    local name = "search_" .. (#self.key_bindings + 1)
    self.key_bindings[#self.key_bindings + 1] = name
    mp.add_forced_key_binding(bind[1], name, bind[2], { repeatable = true })
  end
  mp.add_forced_key_binding("any_unicode", "search_input", function(...)
    self:text_input(...)
  end, { repeatable = true, complex = true })
  self.key_bindings[#self.key_bindings + 1] = "search_input"
end

function em:undefine_key_bindings()
  for _, name in ipairs(self.key_bindings) do
    mp.remove_key_binding(name)
  end
  self.key_bindings = {}
end

-------------------------------------------------------------------------------
--                           END ORIGINAL MPV CODE                           --
-------------------------------------------------------------------------------

return em