-- by maoiscat
-- email:valarmor@163.com
-- https://github.com/maoiscat/mpv-osc-morden

local assdraw = require 'mp.assdraw'
local msg = require 'mp.msg'
local opt = require 'mp.options'
local utils = require 'mp.utils'

--
-- Parameters
--
-- default user option values
-- may change them in osc.conf
local user_opts = {
   showwindowed = true,        -- show OSC when windowed?
   showfullscreen = true,      -- show OSC when fullscreen?
   scalewindowed = 1,          -- scaling of the controller when windowed
   scalefullscreen = 1,        -- scaling of the controller when fullscreen
   scaleforcedwindow = 1,      -- scaling when rendered on a forced window
   vidscale = false,           -- scale the controller with the video?
   hidetimeout = 1000,         -- duration in ms until the OSC hides if no
   -- mouse movement. enforced non-negative for the
   -- user, but internally negative is 'always-on'.
   fadeduration = 500,         -- duration of fade out in ms, 0 = no fade
   minmousemove = 3,           -- minimum amount of pixels the mouse has to
   -- move between ticks to make the OSC show up
   iamaprogrammer = false,     -- use native mpv values and disable OSC
   -- internal track list management (and some
   -- functions that depend on it)
   font = 'mpv-osd-symbols',	-- default osc font
   seekbarhandlesize = 1.0,	-- size ratio of the slider handle, range 0 ~ 1
   seekrange = true,			-- show seekrange overlay
   seekrangealpha = 128,      	-- transparency of seekranges
   seekbarkeyframes = true,    -- use keyframes when dragging the seekbar
   title = '${media-title}',   -- string compatible with property-expansion
   -- to be shown as OSC title
   showtitle = true,			-- show title and no hide timeout on pause
   timetotal = true,          	-- display total time instead of remaining time?
   visibility = 'auto',        -- only used at init to set visibility_mode(...)
   windowcontrols = 'auto',    -- whether to show window controls
   language = 'eng',			-- eng=English, chs=Chinese
}

-- Localization
local language = {
   ['eng'] = {
      welcome = '{\\fs24\\1c&H0&\\3c&HFFFFFF&}Drop files or URLs to play here.',  -- this text appears when mpv starts
      off = 'OFF',
      na = 'n/a',
      none = 'none',
      video = 'Video',
      audio = 'Audio',
      subtitle = 'Subtitle',
      available = 'Available ',
      track = ' Tracks:',
      playlist = 'Playlist',
      nolist = 'Empty playlist.',
      chapter = 'Chapter',
      nochapter = 'No chapters.',
   },
   ['chs'] = {
      welcome = '{\\1c&H00\\bord0\\fs30\\fn微软雅黑 light\\fscx125}MPV{\\fscx100} 播放器',  -- this text appears when mpv starts
      off = '关闭',
      na = 'n/a',
      none = '无',
      video = '视频',
      audio = '音频',
      subtitle = '字幕',
      available = '可选',
      track = ':',
      playlist = '播放列表',
      nolist = '无列表信息',
      chapter = '章节',
      nochapter = '无章节信息',
   }
}
-- read options from config and command-line
opt.read_options(user_opts, 'osc', function(list) update_options(list) end)
-- apply lang opts
local texts = language[user_opts.language]
local osc_param = { -- calculated by osc_init()
   playresy = 0,                           -- canvas size Y
   playresx = 0,                           -- canvas size X
   display_aspect = 1,
   unscaled_y = 0,
   areas = {},
}

local osc_styles = {
   TransBg = '{\\blur100\\bord140\\1c&H000000&\\3c&H000000&}',
   SeekbarBg = '{\\blur0\\bord0\\1c&HFFFFFF&}',
   SeekbarFg = '{\\blur1\\bord1\\1c&HE39C42&}',
   Ctrl1 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs36\\fnmaterial-design-iconic-font}',
   Ctrl2 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs24\\fnmaterial-design-iconic-font}',
   Ctrl3 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs24\\fnmaterial-design-iconic-font}',
   Time = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&H000000&\\fs17\\fn' .. user_opts.font .. '}',
   Tooltip = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H000000&\\fs18\\fn' .. user_opts.font .. '}',
   Title = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H0\\fs48\\q2\\fn' .. user_opts.font .. '}',
   WinCtrl = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H0\\fs20\\fnmpv-osd-symbols}',
   elementDown = '{\\1c&H999999&}',
}

-- internal states, do not touch
local state = {
   showtime,                               -- time of last invocation (last mouse move)
   osc_visible = false,
   anistart,                               -- time when the animation started
   anitype,                                -- current type of animation
   animation,                              -- current animation alpha
   mouse_down_counter = 0,                 -- used for softrepeat
   active_element = nil,                   -- nil = none, 0 = background, 1+ = see elements[]
   active_event_source = nil,              -- the 'button' that issued the current event
   rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time
   mp_screen_sizeX, mp_screen_sizeY,       -- last screen-resolution, to detect resolution changes to issue reINITs
   initREQ = false,                        -- is a re-init request pending?
   last_mouseX, last_mouseY,               -- last mouse position, to detect significant mouse movement
   mouse_in_window = false,
   message_text,
   message_hide_timer,
   fullscreen = false,
   tick_timer = nil,
   tick_last_time = 0,                     -- when the last tick() was run
   hide_timer = nil,
   cache_state = nil,
   idle = false,
   enabled = true,
   input_enabled = true,
   showhide_enabled = false,
   dmx_cache = 0,
   border = true,
   maximized = false,
   osd = mp.create_osd_overlay('ass-events'),
   lastvisibility = user_opts.visibility,	-- save last visibility on pause if showtitle
}

local window_control_box_width = 138
local tick_delay = 0.03

--
-- Helperfunctions
--

function set_osd(res_x, res_y, text)
   if state.osd.res_x == res_x and
      state.osd.res_y == res_y and
      state.osd.data == text then
      return
   end
   state.osd.res_x = res_x
   state.osd.res_y = res_y
   state.osd.data = text
   state.osd.z = 1000
   state.osd:update()
end

-- scale factor for translating between real and virtual ASS coordinates
function get_virt_scale_factor()
   local w, h = mp.get_osd_size()
   if w <= 0 or h <= 0 then
      return 0, 0
   end
   return osc_param.playresx / w, osc_param.playresy / h
end

-- return mouse position in virtual ASS coordinates (playresx/y)
function get_virt_mouse_pos()
   if state.mouse_in_window then
      local sx, sy = get_virt_scale_factor()
      local x, y = mp.get_mouse_pos()
      return x * sx, y * sy
   else
      return -1, -1
   end
end

function set_virt_mouse_area(x0, y0, x1, y1, name)
   local sx, sy = get_virt_scale_factor()
   mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name)
end

function scale_value(x0, x1, y0, y1, val)
   local m = (y1 - y0) / (x1 - x0)
   local b = y0 - (m * x0)
   return (m * val) + b
end

-- returns hitbox spanning coordinates (top left, bottom right corner)
-- according to alignment
function get_hitbox_coords(x, y, an, w, h)

   local alignments = {
      [1] = function () return x, y-h, x+w, y end,
      [2] = function () return x-(w/2), y-h, x+(w/2), y end,
      [3] = function () return x-w, y-h, x, y end,

      [4] = function () return x, y-(h/2), x+w, y+(h/2) end,
      [5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end,
      [6] = function () return x-w, y-(h/2), x, y+(h/2) end,

      [7] = function () return x, y, x+w, y+h end,
      [8] = function () return x-(w/2), y, x+(w/2), y+h end,
      [9] = function () return x-w, y, x, y+h end,
   }

   return alignments[an]()
end

function get_hitbox_coords_geo(geometry)
   return get_hitbox_coords(geometry.x, geometry.y, geometry.an,
                            geometry.w, geometry.h)
end

function get_element_hitbox(element)
   return element.hitbox.x1, element.hitbox.y1,
      element.hitbox.x2, element.hitbox.y2
end

function mouse_hit(element)
   return mouse_hit_coords(get_element_hitbox(element))
end

function mouse_hit_coords(bX1, bY1, bX2, bY2)
   local mX, mY = get_virt_mouse_pos()
   return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2)
end

function limit_range(min, max, val)
   if val > max then
      val = max
   elseif val < min then
      val = min
   end
   return val
end

-- translate value into element coordinates
function get_slider_ele_pos_for(element, val)

   local ele_pos = scale_value(
      element.slider.min.value, element.slider.max.value,
      element.slider.min.ele_pos, element.slider.max.ele_pos,
      val)

   return limit_range(
      element.slider.min.ele_pos, element.slider.max.ele_pos,
      ele_pos)
end

-- translates global (mouse) coordinates to value
function get_slider_value_at(element, glob_pos)

   local val = scale_value(
      element.slider.min.glob_pos, element.slider.max.glob_pos,
      element.slider.min.value, element.slider.max.value,
      glob_pos)

   return limit_range(
      element.slider.min.value, element.slider.max.value,
      val)
end

-- get value at current mouse position
function get_slider_value(element)
   return get_slider_value_at(element, get_virt_mouse_pos())
end

function countone(val)
   if not (user_opts.iamaprogrammer) then
      val = val + 1
   end
   return val
end

-- multiplies two alpha values, formular can probably be improved
function mult_alpha(alphaA, alphaB)
   return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255)
end

function add_area(name, x1, y1, x2, y2)
   -- create area if needed
   if (osc_param.areas[name] == nil) then
      osc_param.areas[name] = {}
   end
   table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2})
end

function ass_append_alpha(ass, alpha, modifier)
   local ar = {}

   for ai, av in pairs(alpha) do
      av = mult_alpha(av, modifier)
      if state.animation then
         av = mult_alpha(av, state.animation)
      end
      ar[ai] = av
   end

   ass:append(string.format('{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}',
                            ar[1], ar[2], ar[3], ar[4]))
end

function ass_draw_cir_cw(ass, x, y, r)
   ass:round_rect_cw(x-r, y-r, x+r, y+r, r)
end

function ass_draw_rr_h_cw(ass, x0, y0, x1, y1, r1, hexagon, r2)
   if hexagon then
      ass:hexagon_cw(x0, y0, x1, y1, r1, r2)
   else
      ass:round_rect_cw(x0, y0, x1, y1, r1, r2)
   end
end

function ass_draw_rr_h_ccw(ass, x0, y0, x1, y1, r1, hexagon, r2)
   if hexagon then
      ass:hexagon_ccw(x0, y0, x1, y1, r1, r2)
   else
      ass:round_rect_ccw(x0, y0, x1, y1, r1, r2)
   end
end


--
-- Tracklist Management
--

local nicetypes = {video = texts.video, audio = texts.audio, sub = texts.subtitle}

-- updates the OSC internal playlists, should be run each time the track-layout changes
function update_tracklist()
   local tracktable = mp.get_property_native('track-list', {})

   -- by osc_id
   tracks_osc = {}
   tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {}
   -- by mpv_id
   tracks_mpv = {}
   tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {}
   for n = 1, #tracktable do
      if not (tracktable[n].type == 'unknown') then
         local type = tracktable[n].type
         local mpv_id = tonumber(tracktable[n].id)

         -- by osc_id
         table.insert(tracks_osc[type], tracktable[n])

         -- by mpv_id
         tracks_mpv[type][mpv_id] = tracktable[n]
         tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type]
      end
   end
end

-- return a nice list of tracks of the given type (video, audio, sub)
function get_tracklist(type)
   local msg = texts.available .. nicetypes[type] .. texts.track
   if #tracks_osc[type] == 0 then
      msg = msg .. texts.none
   else
      for n = 1, #tracks_osc[type] do
         local track = tracks_osc[type][n]
         local lang, title, selected = 'unknown', '', '○'
         if not(track.lang == nil) then lang = track.lang end
         if not(track.title == nil) then title = track.title end
         if (track.id == tonumber(mp.get_property(type))) then
            selected = '●'
         end
         msg = msg..'\n'..selected..' '..n..': ['..lang..'] '..title
      end
   end
   return msg
end

-- relatively change the track of given <type> by <next> tracks
--(+1 -> next, -1 -> previous)
function set_track(type, next)
   local current_track_mpv, current_track_osc
   if (mp.get_property(type) == 'no') then
      current_track_osc = 0
   else
      current_track_mpv = tonumber(mp.get_property(type))
      current_track_osc = tracks_mpv[type][current_track_mpv].osc_id
   end
   local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1)
   local new_track_mpv
   if new_track_osc == 0 then
      new_track_mpv = 'no'
   else
      new_track_mpv = tracks_osc[type][new_track_osc].id
   end

   mp.commandv('set', type, new_track_mpv)

   --	if (new_track_osc == 0) then
   --        show_message(nicetypes[type] .. ' Track: none')
   --    else
   --        show_message(nicetypes[type]  .. ' Track: '
   --            .. new_track_osc .. '/' .. #tracks_osc[type]
   --            .. ' ['.. (tracks_osc[type][new_track_osc].lang or 'unknown') ..'] '
   --            .. (tracks_osc[type][new_track_osc].title or ''))
   --    end
end

-- get the currently selected track of <type>, OSC-style counted
function get_track(type)
   local track = mp.get_property(type)
   if track ~= 'no' and track ~= nil then
      local tr = tracks_mpv[type][tonumber(track)]
      if tr then
         return tr.osc_id
      end
   end
   return 0
end

-- WindowControl helpers
function window_controls_enabled()
   val = user_opts.windowcontrols
   if val == 'auto' then
      return (not state.border) or state.fullscreen
   else
      return val ~= 'no'
   end
end

--
-- Element Management
--

local elements = {}

function prepare_elements()

   -- remove elements without layout or invisble
   local elements2 = {}
   for n, element in pairs(elements) do
      if not (element.layout == nil) and (element.visible) then
         table.insert(elements2, element)
      end
   end
   elements = elements2

   function elem_compare (a, b)
      return a.layout.layer < b.layout.layer
   end

   table.sort(elements, elem_compare)


   for _,element in pairs(elements) do

      local elem_geo = element.layout.geometry

      -- Calculate the hitbox
      local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo)
      element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}

      local style_ass = assdraw.ass_new()

      -- prepare static elements
      style_ass:append('{}') -- hack to troll new_event into inserting a \n
      style_ass:new_event()
      style_ass:pos(elem_geo.x, elem_geo.y)
      style_ass:an(elem_geo.an)
      style_ass:append(element.layout.style)

      element.style_ass = style_ass

      local static_ass = assdraw.ass_new()


      if (element.type == 'box') then
         --draw box
         static_ass:draw_start()
         ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h,
                          element.layout.box.radius, element.layout.box.hexagon)
         static_ass:draw_stop()

      elseif (element.type == 'slider') then
         --draw static slider parts
         local slider_lo = element.layout.slider
         -- calculate positions of min and max points
         element.slider.min.ele_pos = user_opts.seekbarhandlesize * elem_geo.h / 2
         element.slider.max.ele_pos = elem_geo.w - element.slider.min.ele_pos
         element.slider.min.glob_pos = element.hitbox.x1 + element.slider.min.ele_pos
         element.slider.max.glob_pos = element.hitbox.x1 + element.slider.max.ele_pos

         static_ass:draw_start()
         -- a hack which prepares the whole slider area to allow center placements such like an=5
         static_ass:rect_cw(0, 0, elem_geo.w, elem_geo.h)
         static_ass:rect_ccw(0, 0, elem_geo.w, elem_geo.h)
         -- marker nibbles
         if not (element.slider.markerF == nil) and (slider_lo.gap > 0) then
            local markers = element.slider.markerF()
            for _,marker in pairs(markers) do
               if (marker >= element.slider.min.value) and (marker <= element.slider.max.value) then
                  local s = get_slider_ele_pos_for(element, marker)
                  if (slider_lo.gap > 5) then -- draw triangles
                     --top
                     if (slider_lo.nibbles_top) then
                        static_ass:move_to(s - 3, slider_lo.gap - 5)
                        static_ass:line_to(s + 3, slider_lo.gap - 5)
                        static_ass:line_to(s, slider_lo.gap - 1)
                     end
                     --bottom
                     if (slider_lo.nibbles_bottom) then
                        static_ass:move_to(s - 3, elem_geo.h - slider_lo.gap + 5)
                        static_ass:line_to(s, elem_geo.h - slider_lo.gap + 1)
                        static_ass:line_to(s + 3, elem_geo.h - slider_lo.gap + 5)
                     end
                  else -- draw 2x1px nibbles
                     --top
                     if (slider_lo.nibbles_top) then
                        static_ass:rect_cw(s - 1, 0, s + 1, slider_lo.gap);
                     end
                     --bottom
                     if (slider_lo.nibbles_bottom) then
                        static_ass:rect_cw(s - 1, elem_geo.h-slider_lo.gap, s + 1, elem_geo.h);
                     end
                  end
               end
            end
         end
      end

      element.static_ass = static_ass

      -- if the element is supposed to be disabled,
      -- style it accordingly and kill the eventresponders
      if not (element.enabled) then
         element.layout.alpha[1] = 136
         element.eventresponder = nil
      end
   end
end

--
-- Element Rendering
--
function render_elements(master_ass)

   for n=1, #elements do
      local element = elements[n]
      local style_ass = assdraw.ass_new()
      style_ass:merge(element.style_ass)
      ass_append_alpha(style_ass, element.layout.alpha, 0)

      if element.eventresponder and (state.active_element == n) then
         -- run render event functions
         if not (element.eventresponder.render == nil) then
            element.eventresponder.render(element)
         end
         if mouse_hit(element) then
            -- mouse down styling
            if (element.styledown) then
               style_ass:append(osc_styles.elementDown)
            end
            if (element.softrepeat) and (state.mouse_down_counter >= 15
                                         and state.mouse_down_counter % 5 == 0) then

               element.eventresponder[state.active_event_source..'_down'](element)
            end
            state.mouse_down_counter = state.mouse_down_counter + 1
         end
      end

      local elem_ass = assdraw.ass_new()
      elem_ass:merge(style_ass)
      
      if not (element.type == 'button') then
         elem_ass:merge(element.static_ass)
      end

      if (element.type == 'slider') then

         local slider_lo = element.layout.slider
         local elem_geo = element.layout.geometry
         local s_min = element.slider.min.value
         local s_max = element.slider.max.value
         -- draw pos marker
         local pos = element.slider.posF()
         local seekRanges = element.slider.seekRangesF()
         local rh = user_opts.seekbarhandlesize * elem_geo.h / 2 -- Handle radius
         local xp
         
         if pos then
            xp = get_slider_ele_pos_for(element, pos)
            ass_draw_cir_cw(elem_ass, xp, elem_geo.h/2, rh)
            elem_ass:rect_cw(0, slider_lo.gap, xp, elem_geo.h - slider_lo.gap)
         end

         if seekRanges then
            elem_ass:draw_stop()
            elem_ass:merge(element.style_ass)
            ass_append_alpha(elem_ass, element.layout.alpha, user_opts.seekrangealpha)
            elem_ass:merge(element.static_ass)

            for _,range in pairs(seekRanges) do
               local pstart = get_slider_ele_pos_for(element, range['start'])
               local pend = get_slider_ele_pos_for(element, range['end'])
               elem_ass:rect_cw(pstart - rh, slider_lo.gap, pend + rh, elem_geo.h - slider_lo.gap)
            end
         end

         elem_ass:draw_stop()
         
         -- add tooltip
         if not (element.slider.tooltipF == nil) then
            if mouse_hit(element) then
               local sliderpos = get_slider_value(element)
               local tooltiplabel = element.slider.tooltipF(sliderpos)
               local an = slider_lo.tooltip_an
               local ty
               if (an == 2) then
                  ty = element.hitbox.y1
               else
                  ty = element.hitbox.y1 + elem_geo.h/2
               end

               local tx = get_virt_mouse_pos()
               if (slider_lo.adjust_tooltip) then
                  if (an == 2) then
                     if (sliderpos < (s_min + 3)) then
                        an = an - 1
                     elseif (sliderpos > (s_max - 3)) then
                        an = an + 1
                     end
                  elseif (sliderpos > (s_max-s_min)/2) then
                     an = an + 1
                     tx = tx - 5
                  else
                     an = an - 1
                     tx = tx + 10
                  end
               end

               -- tooltip label
               elem_ass:new_event()
               elem_ass:pos(tx, ty)
               elem_ass:an(an)
               elem_ass:append(slider_lo.tooltip_style)
               ass_append_alpha(elem_ass, slider_lo.alpha, 0)
               elem_ass:append(tooltiplabel)
            end
         end

      elseif (element.type == 'button') then

         local buttontext
         if type(element.content) == 'function' then
            buttontext = element.content() -- function objects
         elseif not (element.content == nil) then
            buttontext = element.content -- text objects
         end
         
         buttontext = buttontext:gsub(':%((.?.?.?)%) unknown ', ':%(%1%)')  --gsub('%) unknown %(\'', '')

         local maxchars = element.layout.button.maxchars
         -- 认为1个中文字符约等于1.5个英文字符
         -- local charcount = buttontext:len()-  (buttontext:len()-select(2, buttontext:gsub('[^\128-\193]', '')))/1.5
         local charcount = (buttontext:len() + select(2, buttontext:gsub('[^\128-\193]', ''))*2) / 3
         if not (maxchars == nil) and (charcount > maxchars) then
            local limit = math.max(0, maxchars - 3)
            if (charcount > limit) then
               while (charcount > limit) do
                  buttontext = buttontext:gsub('.[\128-\191]*$', '')
                  charcount = (buttontext:len() + select(2, buttontext:gsub('[^\128-\193]', ''))*2) / 3
               end
               buttontext = buttontext .. '...'
            end
         end

         elem_ass:append(buttontext)
         
         -- add tooltip
         if not (element.tooltipF == nil) and element.enabled then
            if mouse_hit(element) then
               local tooltiplabel = element.tooltipF
               local an = 1
               local ty = element.hitbox.y1
               local tx = get_virt_mouse_pos()
               
               if ty < osc_param.playresy / 2 then
                  ty = element.hitbox.y2
                  an = 7
               end

               -- tooltip label
               if type(element.tooltipF) == 'function' then
                  tooltiplabel = element.tooltipF()
               else
                  tooltiplabel = element.tooltipF
               end
               elem_ass:new_event()
               elem_ass:pos(tx, ty)
               elem_ass:an(an)
               elem_ass:append(element.tooltip_style)
               elem_ass:append(tooltiplabel)
            end
         end
      end

      master_ass:merge(elem_ass)
   end
end

--
-- Message display
--

-- pos is 1 based
function limited_list(prop, pos)
   local proplist = mp.get_property_native(prop, {})
   local count = #proplist
   if count == 0 then
      return count, proplist
   end

   local fs = tonumber(mp.get_property('options/osd-font-size'))
   local max = math.ceil(osc_param.unscaled_y*0.75 / fs)
   if max % 2 == 0 then
      max = max - 1
   end
   local delta = math.ceil(max / 2) - 1
   local begi = math.max(math.min(pos - delta, count - max + 1), 1)
   local endi = math.min(begi + max - 1, count)

   local reslist = {}
   for i=begi, endi do
      local item = proplist[i]
      item.current = (i == pos) and true or nil
      table.insert(reslist, item)
   end
   return count, reslist
end

function get_playlist()
   local pos = mp.get_property_number('playlist-pos', 0) + 1
   local count, limlist = limited_list('playlist', pos)
   if count == 0 then
      return texts.nolist
   end

   local message = string.format(texts.playlist .. ' [%d/%d]:\n', pos, count)
   for i, v in ipairs(limlist) do
      local title = v.title
      local _, filename = utils.split_path(v.filename)
      if title == nil then
         title = filename
      end
      message = string.format('%s %s %s\n', message,
                              (v.current and '●' or '○'), title)
   end
   return message
end

function get_chapterlist()
   local pos = mp.get_property_number('chapter', 0) + 1
   local count, limlist = limited_list('chapter-list', pos)
   if count == 0 then
      return texts.nochapter
   end

   local message = string.format(texts.chapter.. ' [%d/%d]:\n', pos, count)
   for i, v in ipairs(limlist) do
      local time = mp.format_time(v.time)
      local title = v.title
      if title == nil then
         title = string.format(texts.chapter .. ' %02d', i)
      end
      message = string.format('%s[%s] %s %s\n', message, time,
                              (v.current and '●' or '○'), title)
   end
   return message
end

function show_message(text, duration)

   --print('text: '..text..'   duration: ' .. duration)
   if duration == nil then
      duration = tonumber(mp.get_property('options/osd-duration')) / 1000
   elseif not type(duration) == 'number' then
      print('duration: ' .. duration)
   end

   -- cut the text short, otherwise the following functions
   -- may slow down massively on huge input
   text = string.sub(text, 0, 4000)

   -- replace actual linebreaks with ASS linebreaks
   text = string.gsub(text, '\n', '\\N')

   state.message_text = text

   if not state.message_hide_timer then
      state.message_hide_timer = mp.add_timeout(0, request_tick)
   end
   state.message_hide_timer:kill()
   state.message_hide_timer.timeout = duration
   state.message_hide_timer:resume()
   request_tick()
end

function render_message(ass)
   if state.message_hide_timer and state.message_hide_timer:is_enabled() and
      state.message_text
   then
      local _, lines = string.gsub(state.message_text, '\\N', '')

      local fontsize = tonumber(mp.get_property('options/osd-font-size'))
      local outline = tonumber(mp.get_property('options/osd-border-size'))
      local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize)
      local counterscale = osc_param.playresy / osc_param.unscaled_y

      fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1)
      outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1)

      local style = '{\\bord' .. outline .. '\\fs' .. fontsize .. '}'


      ass:new_event()
      ass:append(style .. state.message_text)
   else
      state.message_text = nil
   end
end

--
-- Initialisation and Layout
--

function new_element(name, type)
   elements[name] = {}
   elements[name].type = type

   -- add default stuff
   elements[name].eventresponder = {}
   elements[name].visible = true
   elements[name].enabled = true
   elements[name].softrepeat = false
   elements[name].styledown = (type == 'button')
   elements[name].state = {}

   if (type == 'slider') then
      elements[name].slider = {min = {value = 0}, max = {value = 100}}
   end


   return elements[name]
end

function add_layout(name)
   if not (elements[name] == nil) then
      -- new layout
      elements[name].layout = {}

      -- set layout defaults
      elements[name].layout.layer = 50
      elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255}

      if (elements[name].type == 'button') then
         elements[name].layout.button = {
            maxchars = nil,
         }
      elseif (elements[name].type == 'slider') then
         -- slider defaults
         elements[name].layout.slider = {
            border = 1,
            gap = 1,
            nibbles_top = true,
            nibbles_bottom = true,
            adjust_tooltip = true,
            tooltip_style = '',
            tooltip_an = 2,
            alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255},
         }
      elseif (elements[name].type == 'box') then
         elements[name].layout.box = {radius = 0, hexagon = false}
      end

      return elements[name].layout
   else
      msg.error('Can\'t add_layout to element \''..name..'\', doesn\'t exist.')
   end
end

-- Window Controls
function window_controls()
   local wc_geo = {
      x = 0,
      y = 32,
      an = 1,
      w = osc_param.playresx,
      h = 32,
   }

   local controlbox_w = window_control_box_width
   local titlebox_w = wc_geo.w - controlbox_w

   -- Default alignment is 'right'
   local controlbox_left = wc_geo.w - controlbox_w
   local titlebox_left = wc_geo.x
   local titlebox_right = wc_geo.w - controlbox_w

   add_area('window-controls',
            get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an,
                              controlbox_w, wc_geo.h))

   local lo

   local button_y = wc_geo.y - (wc_geo.h / 2)
   local first_geo =
      {x = controlbox_left + 27, y = button_y, an = 5, w = 40, h = wc_geo.h}
   local second_geo =
      {x = controlbox_left + 69, y = button_y, an = 5, w = 40, h = wc_geo.h}
   local third_geo =
      {x = controlbox_left + 115, y = button_y, an = 5, w = 40, h = wc_geo.h}

   -- Window control buttons use symbols in the custom mpv osd font
   -- because the official unicode codepoints are sufficiently
   -- exotic that a system might lack an installed font with them,
   -- and libass will complain that they are not present in the
   -- default font, even if another font with them is available.

   -- Close: ??
   ne = new_element('close', 'button')
   ne.content = '\238\132\149'
   ne.eventresponder['mbtn_left_up'] =
      function () mp.commandv('quit') end
   lo = add_layout('close')
   lo.geometry = third_geo
   lo.style = osc_styles.WinCtrl
   lo.alpha[3] = 0

   -- Minimize: ??
   ne = new_element('minimize', 'button')
   ne.content = '\\n\238\132\146'
   ne.eventresponder['mbtn_left_up'] =
      function () mp.commandv('cycle', 'window-minimized') end
   lo = add_layout('minimize')
   lo.geometry = first_geo
   lo.style = osc_styles.WinCtrl
   lo.alpha[3] = 0
   
   -- Maximize: ?? /??
   ne = new_element('maximize', 'button')
   if state.maximized or state.fullscreen then
      ne.content = '\238\132\148'
   else
      ne.content = '\238\132\147'
   end
   ne.eventresponder['mbtn_left_up'] =
      function ()
         if state.fullscreen then
            mp.commandv('cycle', 'fullscreen')
         else
            mp.commandv('cycle', 'window-maximized')
         end
      end
   lo = add_layout('maximize')
   lo.geometry = second_geo
   lo.style = osc_styles.WinCtrl
   lo.alpha[3] = 0
end

--
-- Layouts
--

local layouts = {}

-- Default layout
layouts = function ()

   local osc_geo = {w, h}

   osc_geo.w = osc_param.playresx
   osc_geo.h = 180

   -- origin of the controllers, left/bottom corner
   local posX = 0
   local posY = osc_param.playresy

   osc_param.areas = {} -- delete areas

   -- area for active mouse input
   add_area('input', get_hitbox_coords(posX, posY, 1, osc_geo.w, 104))

   -- area for show/hide
   add_area('showhide', 0, 0, osc_param.playresx, osc_param.playresy)

   -- fetch values
   local osc_w, osc_h=
      osc_geo.w, osc_geo.h

   --
   -- Controller Background
   --
   local lo

   new_element('TransBg', 'box')
   lo = add_layout('TransBg')
   lo.geometry = {x = posX, y = posY, an = 7, w = osc_w, h = 1}
   lo.style = osc_styles.TransBg
   lo.layer = 10
   lo.alpha[3] = 0
   
   --
   -- Alignment
   --
   local refX = osc_w / 2
   local refY = posY
   local geo
   
   --
   -- Seekbar
   --
   new_element('bgbar1', 'box')
   lo = add_layout('bgbar1')
   lo.geometry = {x = refX , y = refY - 96 , an = 5, w = osc_geo.w - 50, h = 2}
   lo.layer = 13
   lo.style = osc_styles.SeekbarBg
   lo.alpha[1] = 128
   lo.alpha[3] = 128

   lo = add_layout('seekbar')
   lo.geometry = {x = refX, y = refY - 96 , an = 5, w = osc_geo.w - 50, h = 16}
   lo.style = osc_styles.SeekbarFg
   lo.slider.gap = 7
   lo.slider.tooltip_style = osc_styles.Tooltip
   lo.slider.tooltip_an = 2
   
   -- buttons
   lo = add_layout('pl_prev')
   lo.geometry = {x = refX - 120, y = refY - 40 , an = 5, w = 30, h = 24}
   lo.style = osc_styles.Ctrl2

   lo = add_layout('skipback')
   lo.geometry = {x = refX - 60, y = refY - 40 , an = 5, w = 30, h = 24}
   lo.style = osc_styles.Ctrl2

   
   lo = add_layout('playpause')
   lo.geometry = {x = refX, y = refY - 40 , an = 5, w = 45, h = 45}
   lo.style = osc_styles.Ctrl1	

   lo = add_layout('skipfrwd')
   lo.geometry = {x = refX + 60, y = refY - 40 , an = 5, w = 30, h = 24}
   lo.style = osc_styles.Ctrl2	

   lo = add_layout('pl_next')
   lo.geometry = {x = refX + 120, y = refY - 40 , an = 5, w = 30, h = 24}
   lo.style = osc_styles.Ctrl2


   -- Time
   lo = add_layout('tc_left')
   lo.geometry = {x = 25, y = refY - 84, an = 7, w = 64, h = 20}
   lo.style = osc_styles.Time	
   

   lo = add_layout('tc_right')
   lo.geometry = {x = osc_geo.w - 25 , y = refY -84, an = 9, w = 64, h = 20}
   lo.style = osc_styles.Time	

   lo = add_layout('cy_audio')
   lo.geometry = {x = 37, y = refY - 40, an = 5, w = 24, h = 24}
   lo.style = osc_styles.Ctrl3	
   
   lo = add_layout('cy_sub')
   lo.geometry = {x = 87, y = refY - 40, an = 5, w = 24, h = 24}
   lo.style = osc_styles.Ctrl3

   lo = add_layout('tog_fs')
   lo.geometry = {x = osc_geo.w - 37, y = refY - 40, an = 5, w = 24, h = 24}
   lo.style = osc_styles.Ctrl3    

   lo = add_layout('tog_info')
   lo.geometry = {x = osc_geo.w - 87, y = refY - 40, an = 5, w = 24, h = 24}
   lo.style = osc_styles.Ctrl3
   
   geo = { x = 25, y = refY - 132, an = 1, w = osc_geo.w - 50, h = 48 }
   lo = add_layout('title')
   lo.geometry = geo
   lo.style = string.format('%s{\\clip(%f,%f,%f,%f)}', osc_styles.Title,
                            geo.x, geo.y - geo.h, geo.x + geo.w , geo.y)
   lo.alpha[3] = 0
end

-- Validate string type user options
function validate_user_opts()
   if user_opts.windowcontrols ~= 'auto' and
      user_opts.windowcontrols ~= 'yes' and
      user_opts.windowcontrols ~= 'no' then
      msg.warn('windowcontrols cannot be \'' ..
               user_opts.windowcontrols .. '\'. Ignoring.')
      user_opts.windowcontrols = 'auto'
   end
end

function update_options(list)
   validate_user_opts()
   request_tick()
   visibility_mode(user_opts.visibility, true)
   request_init()
end

-- OSC INIT
function osc_init()
   msg.debug('osc_init')

   -- set canvas resolution according to display aspect and scaling setting
   local baseResY = 720
   local display_w, display_h, display_aspect = mp.get_osd_size()
   local scale = 1

   if (mp.get_property('video') == 'no') then -- dummy/forced window
      scale = user_opts.scaleforcedwindow
   elseif state.fullscreen then
      scale = user_opts.scalefullscreen
   else
      scale = user_opts.scalewindowed
   end

   if user_opts.vidscale then
      osc_param.unscaled_y = baseResY
   else
      osc_param.unscaled_y = display_h
   end
   osc_param.playresy = osc_param.unscaled_y / scale
   if (display_aspect > 0) then
      osc_param.display_aspect = display_aspect
   end
   osc_param.playresx = osc_param.playresy * osc_param.display_aspect

   -- stop seeking with the slider to prevent skipping files
   state.active_element = nil

   elements = {}

   -- some often needed stuff
   local pl_count = mp.get_property_number('playlist-count', 0)
   local have_pl = (pl_count > 1)
   local pl_pos = mp.get_property_number('playlist-pos', 0) + 1
   local have_ch = (mp.get_property_number('chapters', 0) > 0)
   local loop = mp.get_property('loop-playlist', 'no')

   local ne

   -- playlist buttons
   -- prev
   ne = new_element('pl_prev', 'button')

   ne.content = '\xEF\x8E\xB5'
   ne.enabled = (pl_pos > 1) or (loop ~= 'no')
   ne.eventresponder['mbtn_left_up'] =
      function ()
         mp.commandv('playlist-prev', 'weak')
      end
   ne.eventresponder['mbtn_right_up'] =
      function () show_message(get_playlist()) end

   --next
   ne = new_element('pl_next', 'button')

   ne.content = '\xEF\x8E\xB4'
   ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= 'no')
   ne.eventresponder['mbtn_left_up'] =
      function ()
         mp.commandv('playlist-next', 'weak')
      end
   ne.eventresponder['mbtn_right_up'] =
      function () show_message(get_playlist()) end


   --play control buttons
   --playpause
   ne = new_element('playpause', 'button')

   ne.content = function ()
      if mp.get_property('pause') == 'yes' then
         return ('\xEF\x8E\xA7')
      else
         return ('\xEF\x8E\xAA')
      end
   end
   ne.eventresponder['mbtn_left_up'] =
      function () mp.commandv('cycle', 'pause') end
   --ne.eventresponder['mbtn_right_up'] =
   --    function () mp.commandv('script-binding', 'open-file-dialog') end

   --skipback
   ne = new_element('skipback', 'button')

   ne.softrepeat = true
   ne.content = '\xEF\x8E\xA0'
   ne.eventresponder['mbtn_left_down'] =
      --function () mp.command('seek -5') end
      function () mp.commandv('seek', -5, 'relative', 'keyframes') end
   ne.eventresponder['shift+mbtn_left_down'] =
      function () mp.commandv('frame-back-step') end
   ne.eventresponder['mbtn_right_down'] =
      --function () mp.command('seek -60') end
      function () mp.commandv('seek', -60, 'relative', 'keyframes') end

   --skipfrwd
   ne = new_element('skipfrwd', 'button')

   ne.softrepeat = true
   ne.content = '\xEF\x8E\x9F'
   ne.eventresponder['mbtn_left_down'] =
      --function () mp.command('seek +5') end
      function () mp.commandv('seek', 5, 'relative', 'keyframes') end
   ne.eventresponder['shift+mbtn_left_down'] =
      function () mp.commandv('frame-step') end
   ne.eventresponder['mbtn_right_down'] =
      --function () mp.command('seek +60') end
      function () mp.commandv('seek', 60, 'relative', 'keyframes') end

   --
   update_tracklist()
   
   --cy_audio
   ne = new_element('cy_audio', 'button')
   ne.enabled = (#tracks_osc.audio > 0)
   ne.visible = (osc_param.playresx >= 540)
   ne.content = '\xEF\x8E\xB7'
   ne.tooltip_style = osc_styles.Tooltip
   ne.tooltipF = function ()
      local msg = texts.off
      if not (get_track('audio') == 0) then
         msg = (texts.audio .. ' [' .. get_track('audio') .. ' ∕ ' .. #tracks_osc.audio .. '] ')
         local prop = mp.get_property('current-tracks/audio/lang')
         if not prop then
            prop = texts.na
         end
         msg = msg .. '[' .. prop .. ']'
         prop = mp.get_property('current-tracks/audio/title')
         if prop then
            msg = msg .. ' ' .. prop
         end
         return msg
      end
      return msg
   end
   ne.eventresponder['mbtn_left_up'] =
      function () set_track('audio', 1) end
   ne.eventresponder['mbtn_right_up'] =
      function () set_track('audio', -1) end
   ne.eventresponder['mbtn_mid_up'] =
      function () show_message(get_tracklist('audio')) end
   
   --cy_sub
   ne = new_element('cy_sub', 'button')
   ne.enabled = (#tracks_osc.sub > 0)
   ne.visible = (osc_param.playresx >= 600)
   ne.content = '\xEF\x8F\x93'
   ne.tooltip_style = osc_styles.Tooltip
   ne.tooltipF = function ()
      local msg = texts.off
      if not (get_track('sub') == 0) then
         msg = (texts.subtitle .. ' [' .. get_track('sub') .. ' ∕ ' .. #tracks_osc.sub .. '] ')
         local prop = mp.get_property('current-tracks/sub/lang')
         if not prop then
            prop = texts.na
         end
         msg = msg .. '[' .. prop .. ']'
         prop = mp.get_property('current-tracks/sub/title')
         if prop then
            msg = msg .. ' ' .. prop
         end
         return msg
      end
      return msg
   end
   ne.eventresponder['mbtn_left_up'] =
      function () set_track('sub', 1) end
   ne.eventresponder['mbtn_right_up'] =
      function () set_track('sub', -1) end
   ne.eventresponder['mbtn_mid_up'] =
      function () show_message(get_tracklist('sub')) end
   
   --tog_fs
   ne = new_element('tog_fs', 'button')
   ne.content = function ()
      if (state.fullscreen) then
         return ('\xEF\x85\xAC')
      else
         return ('\xEF\x85\xAD')
      end
   end
   ne.visible = (osc_param.playresx >= 540)
   ne.eventresponder['mbtn_left_up'] =
      function () mp.commandv('cycle', 'fullscreen') end

   --tog_info
   ne = new_element('tog_info', 'button')
   ne.content = ''
   ne.visible = (osc_param.playresx >= 600)
   ne.eventresponder['mbtn_left_up'] =
      function () mp.commandv('script-binding', 'stats/display-stats-toggle') end

   -- title
   ne = new_element('title', 'button')
   ne.content = function ()
      local title = mp.command_native({'expand-text', user_opts.title})
      if state.paused then
         title = title:gsub('\\n', ' '):gsub('\\$', ''):gsub('{','\\{')
      else
         title = ' '
      end
      return not (title == '') and title or ' '
   end
   ne.visible = osc_param.playresy >= 320 and user_opts.showtitle
   
   --seekbar
   ne = new_element('seekbar', 'slider')

   ne.enabled = not (mp.get_property('percent-pos') == nil)
   ne.slider.markerF = function ()
      local duration = mp.get_property_number('duration', nil)
      if not (duration == nil) then
         local chapters = mp.get_property_native('chapter-list', {})
         local markers = {}
         for n = 1, #chapters do
            markers[n] = (chapters[n].time / duration * 100)
         end
         return markers
      else
         return {}
      end
   end
   ne.slider.posF =
      function () return mp.get_property_number('percent-pos', nil) end
   ne.slider.tooltipF = function (pos)
      local duration = mp.get_property_number('duration', nil)
      if not ((duration == nil) or (pos == nil)) then
         possec = duration * (pos / 100)
         return mp.format_time(possec)
      else
         return ''
      end
   end
   ne.slider.seekRangesF = function()
      if not user_opts.seekrange then
         return nil
      end
      local cache_state = state.cache_state
      if not cache_state then
         return nil
      end
      local duration = mp.get_property_number('duration', nil)
      if (duration == nil) or duration <= 0 then
         return nil
      end
      local ranges = cache_state['seekable-ranges']
      if #ranges == 0 then
         return nil
      end
      local nranges = {}
      for _, range in pairs(ranges) do
         nranges[#nranges + 1] = {
            ['start'] = 100 * range['start'] / duration,
            ['end'] = 100 * range['end'] / duration,
         }
      end
      return nranges
   end
   ne.eventresponder['mouse_move'] = --keyframe seeking when mouse is dragged
      function (element)
         if not element.state.mbtnleft then return end -- allow drag for mbtnleft only!
         -- mouse move events may pile up during seeking and may still get
         -- sent when the user is done seeking, so we need to throw away
         -- identical seeks
         local seekto = get_slider_value(element)
         if (element.state.lastseek == nil) or
            (not (element.state.lastseek == seekto)) then
            local flags = 'absolute-percent'
            if not user_opts.seekbarkeyframes then
               flags = flags .. '+exact'
            end
            mp.commandv('seek', seekto, flags)
            element.state.lastseek = seekto
         end

      end
   ne.eventresponder['mbtn_left_down'] = --exact seeks on single clicks
      function (element)
         mp.commandv('seek', get_slider_value(element), 'absolute-percent', 'exact')
         element.state.mbtnleft = true
      end
   ne.eventresponder['mbtn_left_up'] =
      function (element) element.state.mbtnleft = false end
   ne.eventresponder['mbtn_right_down'] = --seeks to chapter start
      function (element)
         local duration = mp.get_property_number('duration', nil)
         if not (duration == nil) then
            local chapters = mp.get_property_native('chapter-list', {})
            if #chapters > 0 then
               local pos = get_slider_value(element)
               local ch = #chapters
               for n = 1, ch do
                  if chapters[n].time / duration * 100 >= pos then
                     ch = n - 1
                     break
                  end
               end
               mp.commandv('set', 'chapter', ch - 1)
               --if chapters[ch].title then show_message(chapters[ch].time) end
            end
         end
      end
   ne.eventresponder['reset'] =
      function (element) element.state.lastseek = nil end


   -- tc_left (current pos)
   ne = new_element('tc_left', 'button')
   ne.content = function () return (mp.get_property_osd('playback-time')) end

   -- tc_right (total/remaining time)
   ne = new_element('tc_right', 'button')
   ne.content = function ()
      if (mp.get_property_number('duration', 0) <= 0) then return '--:--:--' end
      if (state.rightTC_trem) then
         return ('-'..mp.get_property_osd('playtime-remaining'))
      else
         return (mp.get_property_osd('duration'))
      end
   end
   ne.eventresponder['mbtn_left_up'] =
      function () state.rightTC_trem = not state.rightTC_trem end

   -- load layout
   layouts()

   -- load window controls
   if window_controls_enabled() then
      window_controls()
   end

   --do something with the elements
   prepare_elements()
end

function shutdown()
   
end

--
-- Other important stuff
--


function show_osc()
   -- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding
   if not state.enabled then return end

   msg.trace('show_osc')
   --remember last time of invocation (mouse move)
   state.showtime = mp.get_time()

   osc_visible(true)

   if (user_opts.fadeduration > 0) then
      state.anitype = nil
   end
end

function hide_osc()
   msg.trace('hide_osc')
   if not state.enabled then
      -- typically hide happens at render() from tick(), but now tick() is
      -- no-op and won't render again to remove the osc, so do that manually.
      state.osc_visible = false
      render_wipe()
   elseif (user_opts.fadeduration > 0) then
      if not(state.osc_visible == false) then
         state.anitype = 'out'
         request_tick()
      end
   else
      osc_visible(false)
   end
end

function osc_visible(visible)
   if state.osc_visible ~= visible then
      state.osc_visible = visible
   end
   request_tick()
end

function pause_state(name, enabled)
   state.paused = enabled
   if user_opts.showtitle then
      if enabled then
         state.lastvisibility = user_opts.visibility
         visibility_mode("always", true)
         show_osc()
      else
         visibility_mode(state.lastvisibility, true)
      end
   end
   request_tick()
end

function cache_state(name, st)
   state.cache_state = st
   request_tick()
end

-- Request that tick() is called (which typically re-renders the OSC).
-- The tick is then either executed immediately, or rate-limited if it was
-- called a small time ago.
function request_tick()
   if state.tick_timer == nil then
      state.tick_timer = mp.add_timeout(0, tick)
   end

   if not state.tick_timer:is_enabled() then
      local now = mp.get_time()
      local timeout = tick_delay - (now - state.tick_last_time)
      if timeout < 0 then
         timeout = 0
      end
      state.tick_timer.timeout = timeout
      state.tick_timer:resume()
   end
end

function mouse_leave()
   if get_hidetimeout() >= 0 then
      hide_osc()
   end
   -- reset mouse position
   state.last_mouseX, state.last_mouseY = nil, nil
   state.mouse_in_window = false
end

function request_init()
   state.initREQ = true
   request_tick()
end

-- Like request_init(), but also request an immediate update
function request_init_resize()
   request_init()
   -- ensure immediate update
   state.tick_timer:kill()
   state.tick_timer.timeout = 0
   state.tick_timer:resume()
end

function render_wipe()
   msg.trace('render_wipe()')
   state.osd:remove()
end

function render()
   msg.trace('rendering')
   local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size()
   local mouseX, mouseY = get_virt_mouse_pos()
   local now = mp.get_time()

   -- check if display changed, if so request reinit
   if not (state.mp_screen_sizeX == current_screen_sizeX
           and state.mp_screen_sizeY == current_screen_sizeY) then

      request_init_resize()

      state.mp_screen_sizeX = current_screen_sizeX
      state.mp_screen_sizeY = current_screen_sizeY
   end

   -- init management
   if state.initREQ then
      osc_init()
      state.initREQ = false

      -- store initial mouse position
      if (state.last_mouseX == nil or state.last_mouseY == nil)
         and not (mouseX == nil or mouseY == nil) then

         state.last_mouseX, state.last_mouseY = mouseX, mouseY
      end
   end


   -- fade animation
   if not(state.anitype == nil) then

      if (state.anistart == nil) then
         state.anistart = now
      end

      if (now < state.anistart + (user_opts.fadeduration/1000)) then

         if (state.anitype == 'in') then --fade in
            osc_visible(true)
            state.animation = scale_value(state.anistart,
                                          (state.anistart + (user_opts.fadeduration/1000)),
                                          255, 0, now)
         elseif (state.anitype == 'out') then --fade out
            state.animation = scale_value(state.anistart,
                                          (state.anistart + (user_opts.fadeduration/1000)),
                                          0, 255, now)
         end

      else
         if (state.anitype == 'out') then
            osc_visible(false)
         end
         state.anistart = nil
         state.animation = nil
         state.anitype =  nil
      end
   else
      state.anistart = nil
      state.animation = nil
      state.anitype =  nil
   end

   --mouse show/hide area
   for k,cords in pairs(osc_param.areas['showhide']) do
      set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'showhide')
   end
   if osc_param.areas['showhide_wc'] then
      for k,cords in pairs(osc_param.areas['showhide_wc']) do
         set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'showhide_wc')
      end
   else
      set_virt_mouse_area(0, 0, 0, 0, 'showhide_wc')
   end
   do_enable_keybindings()

   --mouse input area
   local mouse_over_osc = false

   for _,cords in ipairs(osc_param.areas['input']) do
      if state.osc_visible then -- activate only when OSC is actually visible
         set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'input')
      end
      if state.osc_visible ~= state.input_enabled then
         if state.osc_visible then
            mp.enable_key_bindings('input')
         else
            mp.disable_key_bindings('input')
         end
         state.input_enabled = state.osc_visible
      end

      if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
         mouse_over_osc = true
      end
   end

   if osc_param.areas['window-controls'] then
      for _,cords in ipairs(osc_param.areas['window-controls']) do
         if state.osc_visible then -- activate only when OSC is actually visible
            set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'window-controls')
            mp.enable_key_bindings('window-controls')
         else
            mp.disable_key_bindings('window-controls')
         end

         if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
            mouse_over_osc = true
         end
      end
   end

   if osc_param.areas['window-controls-title'] then
      for _,cords in ipairs(osc_param.areas['window-controls-title']) do
         if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
            mouse_over_osc = true
         end
      end
   end

   -- autohide
   if not (state.showtime == nil) and (get_hidetimeout() >= 0) then
      local timeout = state.showtime + (get_hidetimeout()/1000) - now
      if timeout <= 0 then
         if (state.active_element == nil) and not (mouse_over_osc) then
            hide_osc()
         end
      else
         -- the timer is only used to recheck the state and to possibly run
         -- the code above again
         if not state.hide_timer then
            state.hide_timer = mp.add_timeout(0, tick)
         end
         state.hide_timer.timeout = timeout
         -- re-arm
         state.hide_timer:kill()
         state.hide_timer:resume()
      end
   end


   -- actual rendering
   local ass = assdraw.ass_new()

   -- Messages
   render_message(ass)

   -- actual OSC
   if state.osc_visible then
      render_elements(ass)
   end

   -- submit
   set_osd(osc_param.playresy * osc_param.display_aspect,
           osc_param.playresy, ass.text)
end

--
-- Eventhandling
--

local function element_has_action(element, action)
   return element and element.eventresponder and
      element.eventresponder[action]
end

function process_event(source, what)
   local action = string.format('%s%s', source,
                                what and ('_' .. what) or '')

   if what == 'down' or what == 'press' then

      for n = 1, #elements do

         if mouse_hit(elements[n]) and
            elements[n].eventresponder and
            (elements[n].eventresponder[source .. '_up'] or
             elements[n].eventresponder[action]) then

            if what == 'down' then
               state.active_element = n
               state.active_event_source = source
            end
            -- fire the down or press event if the element has one
            if element_has_action(elements[n], action) then
               elements[n].eventresponder[action](elements[n])
            end

         end
      end

   elseif what == 'up' then

      if elements[state.active_element] then
         local n = state.active_element

         if n == 0 then
            --click on background (does not work)
         elseif element_has_action(elements[n], action) and
            mouse_hit(elements[n]) then

            elements[n].eventresponder[action](elements[n])
         end

         --reset active element
         if element_has_action(elements[n], 'reset') then
            elements[n].eventresponder['reset'](elements[n])
         end

      end
      state.active_element = nil
      state.mouse_down_counter = 0

   elseif source == 'mouse_move' then

      state.mouse_in_window = true

      local mouseX, mouseY = get_virt_mouse_pos()
      if (user_opts.minmousemove == 0) or
         (not ((state.last_mouseX == nil) or (state.last_mouseY == nil)) and
          ((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove)
             or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove)
          )
      ) then
         show_osc()
      end
      state.last_mouseX, state.last_mouseY = mouseX, mouseY

      local n = state.active_element
      if element_has_action(elements[n], action) then
         elements[n].eventresponder[action](elements[n])
      end
      request_tick()
   end
end

function show_logo()
   local osd_w, osd_h = 640, 360
   local logo_x, logo_y = osd_w/2, osd_h/2-20
   local ass = assdraw.ass_new()
   ass:new_event()
   ass:pos(logo_x, logo_y)
   ass:append('{\\1c&H8E348D&\\3c&H0&\\3a&H60&\\blur1\\bord0.5}')
   ass:draw_start()
   ass_draw_cir_cw(ass, 0, 0, 100)
   ass:draw_stop()
   
   ass:new_event()
   ass:pos(logo_x, logo_y)
   ass:append('{\\1c&H632462&\\bord0}')
   ass:draw_start()
   ass_draw_cir_cw(ass, 6, -6, 75)
   ass:draw_stop()

   ass:new_event()
   ass:pos(logo_x, logo_y)
   ass:append('{\\1c&HFFFFFF&\\bord0}')
   ass:draw_start()
   ass_draw_cir_cw(ass, -4, 4, 50)
   ass:draw_stop()
   
   ass:new_event()
   ass:pos(logo_x, logo_y)
   ass:append('{\\1c&H632462&\\bord&}')
   ass:draw_start()
   ass:move_to(-20, -20)
   ass:line_to(23.3, 5)
   ass:line_to(-20, 30)
   ass:draw_stop()
   
   ass:new_event()
   ass:pos(logo_x, logo_y+110)
   ass:an(8)
   ass:append(texts.welcome)
   set_osd(osd_w, osd_h, ass.text)
end

-- called by mpv on every frame
function tick()
   if (not state.enabled) then return end

   if (state.idle) then
      show_logo()
      -- render idle message
      msg.trace('idle message')

      if state.showhide_enabled then
         mp.disable_key_bindings('showhide')
         mp.disable_key_bindings('showhide_wc')
         state.showhide_enabled = false
      end


   elseif (state.fullscreen and user_opts.showfullscreen)
      or (not state.fullscreen and user_opts.showwindowed) then

      -- render the OSC
      render()
   else
      -- Flush OSD
      set_osd(osc_param.playresy, osc_param.playresy, '')
   end

   state.tick_last_time = mp.get_time()

   if state.anitype ~= nil then
      request_tick()
   end
end

function do_enable_keybindings()
   if state.enabled then
      if not state.showhide_enabled then
         mp.enable_key_bindings('showhide', 'allow-vo-dragging+allow-hide-cursor')
         mp.enable_key_bindings('showhide_wc', 'allow-vo-dragging+allow-hide-cursor')
      end
      state.showhide_enabled = true
   end
end

function enable_osc(enable)
   state.enabled = enable
   if enable then
      do_enable_keybindings()
   else
      hide_osc() -- acts immediately when state.enabled == false
      if state.showhide_enabled then
         mp.disable_key_bindings('showhide')
         mp.disable_key_bindings('showhide_wc')
      end
      state.showhide_enabled = false
   end
end

validate_user_opts()

mp.register_event('shutdown', shutdown)
mp.register_event('start-file', request_init)
mp.observe_property('track-list', nil, request_init)
mp.observe_property('playlist', nil, request_init)

mp.register_script_message('osc-message', show_message)
mp.register_script_message('osc-chapterlist', function(dur)
                              show_message(get_chapterlist(), dur)
end)
mp.register_script_message('osc-playlist', function(dur)
                              show_message(get_playlist(), dur)
end)
mp.register_script_message('osc-tracklist', function(dur)
                              local msg = {}
                              for k,v in pairs(nicetypes) do
                                 table.insert(msg, get_tracklist(k))
                              end
                              show_message(table.concat(msg, '\n\n'), dur)
end)

mp.observe_property('fullscreen', 'bool',
                    function(name, val)
                       state.fullscreen = val
                       request_init_resize()
                    end
)
mp.observe_property('border', 'bool',
                    function(name, val)
                       state.border = val
                       request_init_resize()
                    end
)
mp.observe_property('window-maximized', 'bool',
                    function(name, val)
                       state.maximized = val
                       request_init_resize()
                    end
)
mp.observe_property('idle-active', 'bool',
                    function(name, val)
                       state.idle = val
                       request_tick()
                    end
)
mp.observe_property('pause', 'bool', pause_state)
mp.observe_property('demuxer-cache-state', 'native', cache_state)
mp.observe_property('vo-configured', 'bool', function(name, val)
                       request_tick()
end)
mp.observe_property('playback-time', 'number', function(name, val)
                       request_tick()
end)
mp.observe_property('osd-dimensions', 'native', function(name, val)
                       -- (we could use the value instead of re-querying it all the time, but then
                       --  we might have to worry about property update ordering)
                       request_init_resize()
end)

-- mouse show/hide bindings
mp.set_key_bindings({
      {'mouse_move',              function(e) process_event('mouse_move', nil) end},
      {'mouse_leave',             mouse_leave},
                    }, 'showhide', 'force')
mp.set_key_bindings({
      {'mouse_move',              function(e) process_event('mouse_move', nil) end},
      {'mouse_leave',             mouse_leave},
                    }, 'showhide_wc', 'force')
do_enable_keybindings()

--mouse input bindings
mp.set_key_bindings({
      {'mbtn_left',           function(e) process_event('mbtn_left', 'up') end,
       function(e) process_event('mbtn_left', 'down')  end},
      {'mbtn_right',          function(e) process_event('mbtn_right', 'up') end,
       function(e) process_event('mbtn_right', 'down')  end},
      {'mbtn_mid',            function(e) process_event('mbtn_mid', 'up') end,
       function(e) process_event('mbtn_mid', 'down')  end},
      {'wheel_up',            function(e) process_event('wheel_up', 'press') end},
      {'wheel_down',          function(e) process_event('wheel_down', 'press') end},
      {'mbtn_left_dbl',       'ignore'},
      {'mbtn_right_dbl',      'ignore'},
                    }, 'input', 'force')
mp.enable_key_bindings('input')

mp.set_key_bindings({
      {'mbtn_left',           function(e) process_event('mbtn_left', 'up') end,
       function(e) process_event('mbtn_left', 'down')  end},
                    }, 'window-controls', 'force')
mp.enable_key_bindings('window-controls')

function get_hidetimeout()
   if user_opts.visibility == 'always' then
      return -1 -- disable autohide
   end
   return user_opts.hidetimeout
end

function always_on(val)
   if state.enabled then
      if val then
         show_osc()
      else
         hide_osc()
      end
   end
end

-- mode can be auto/always/never/cycle
-- the modes only affect internal variables and not stored on its own.
function visibility_mode(mode, no_osd)
   if mode == 'auto' then
      always_on(false)
      enable_osc(true)
   elseif mode == 'always' then
      enable_osc(true)
      always_on(true)
   elseif mode == 'never' then
      enable_osc(false)
   else
      msg.warn('Ignoring unknown visibility mode \"' .. mode .. '\"')
      return
   end
   
   user_opts.visibility = mode
   
   if not no_osd and tonumber(mp.get_property('osd-level')) >= 1 then
      mp.osd_message('OSC visibility: ' .. mode)
   end

   -- Reset the input state on a mode change. The input state will be
   -- recalcuated on the next render cycle, except in 'never' mode where it
   -- will just stay disabled.
   mp.disable_key_bindings('input')
   mp.disable_key_bindings('window-controls')
   state.input_enabled = false
   request_tick()
end

visibility_mode(user_opts.visibility, true)
mp.register_script_message('osc-visibility', visibility_mode)
mp.add_key_binding(nil, 'visibility', function() visibility_mode('cycle') end)

set_virt_mouse_area(0, 0, 0, 0, 'input')
set_virt_mouse_area(0, 0, 0, 0, 'window-controls')