----------------------------------------------------------------
--globals
----------------------------------------------------------------
function httpTimeshiftHandler(queryType, params)
 return httpTimeshift.handler(queryType, params)
end
----------------------------------------------------------------
--module
----------------------------------------------------------------
local base = _G
module("httpTimeshift")
----------------------------------------------------------------
--locals
----------------------------------------------------------------
local currentData = {}
local eventExecutorTable = {}
----------------------------------------------------------------
--for debug
----------------------------------------------------------------
local debugMode   = false
local debugInFile = false
----------------------------------------------------------------
--interface
----------------------------------------------------------------
function getVersion()
  return "0.8",0.8
end
----------------------------------------------------------------
function getDialogId()
 return 'httpTimeshiftDialog'
end
----------------------------------------------------------------
function isEnabled()
 local val = getConfigVal('enabled') 
 if base.type(val) ~= 'boolean' then
    val = true
 end
 return val
end
----------------------------------------------------------------
function setEnabled(val)
 if base.type(val) == 'boolean' then
   setConfigVal('enabled',val)
 end   
end
------------------------------------------------------------------------------
function isGuiEnabled() 
 local val = getConfigVal('guiEnabled') 
 if base.type(val) ~= 'boolean' then
    val = true
 end
 return val
end
----------------------------------------------------------------
function setGuiEnabled(val)
 if base.type(val) == 'boolean' then
   setConfigVal('guiEnabled',val)
 end   
end
------------------------------------------------------------------------------
function isOldEnabled()
 local val = getConfigVal('oldEnabled') 
 if base.type(val) ~= 'boolean' then
    val = false
 end
 return val
end
----------------------------------------------------------------
function setOldEnabled(val)
 if base.type(val) == 'boolean' then
   setConfigVal('oldEnabled',val)
 end   
end
------------------------------------------------------------------------------
function startRecordMargin()
 local val = base.tonumber(getConfigVal('startMargin') or 0) or 0 
 return val
end
------------------------------------------------------------------------------
function setStartRecordMargin(val)
 if base.type(val) == 'number' then
   if val<0 then val = 0 end
   if val>120 then val = 120 end
   setConfigVal('startMargin',val)
 end   
end
------------------------------------------------------------------------------
function endRecordMargin()
 local val = base.tonumber(getConfigVal('endMargin') or 0) or 0
 return val
end
------------------------------------------------------------------------------
function setEndRecordMargin(val)
 if base.type(val) == 'number' then
   if val<0 then val = 0 end
   if val>120 then val = 120 end
   setConfigVal('endMargin',val)
 end   
end
------------------------------------------------------------------------------
function getConfigFile()
 return 'httpTimeshift.ini'
end
------------------------------------------------------------------------------
function getConfigVal(key)
 return base.m_simpleTV.Config.GetValue(key,getConfigFile())
end
------------------------------------------------------------------------------
function setConfigVal(key,val)
  base.m_simpleTV.Config.SetValue(key,val,getConfigFile())
end
------------------------------------------------------------------------------
function tr(str)
  return base.m_simpleTV.Interface.Translate(str,'simpleTV::httpTimeshift')
end
----------------------------------------------------------------------  	
function Log(message,verbose)
 
 if not debugMode then return end
 
 message = message or 'nil' 
 if debugInFile then 
   base.debug_in_file(message .. '\n')
 else 
   base.m_simpleTV.Logger.WriteToLog(message, verbose or 3, 'httpTimeshift')
 end
end
----------------------------------------------------------------------          
--Helpers
----------------------------------------------------------------------          
local function trim(s)
   return (s:gsub("^%s*(.-)%s*$", "%1"))
end
----------------------------------------------------------------------
local function stopTimer()
  if currentData.TimerId then
     base.m_simpleTV.Timer.DeleteTimer(currentData.TimerId)
	 currentData.TimerId = nil
  end
end
----------------------------------------------------------------------  	
local function reset()
 stopTimer()
 currentData = {}
end
----------------------------------------------------------------------
--Timer handler
----------------------------------------------------------------------  	
function setDelayedPosition()
 currentData.TimerId = nil
 if not currentData.currentPosition then return end
 
 local state = base.m_simpleTV.Control.GetState()
 if state < 1 or state > 5 then return end
   
 local offset = (1 - currentData.currentPosition ) * currentData.CatchupParams.len  
 offset = base.math.floor(offset/1000) * 1000
 
 base.m_simpleTV.Control.SetNewAddressT({timeshiftOffset = offset})
end
----------------------------------------------------------------------
--RawM3UString to table
----------------------------------------------------------------------
local function dispatchOneString(t,str)
 
 if base.type(str) == 'string' then
   local s = str:match('catchup=[\"\'](.-)[\"\']')
   if s then 
     t.catchupType = trim(s)
   end
   
   s = str:match('catchup%-type=[\"\'](.-)[\"\']')
   if s then 
     t.catchupType = trim(s)
   end
   
   s =  str:match("tvg%-rec=[\"\'](%d+)[\"\']")
   if s then
     t.len = base.tonumber(s) * 24*3600*1000 
   end 
   
   s =  str:match("catchup%-days=[\"\'](%d+)[\"\']") or str:match("catchup%-days=(%d+)")
   if s then
     t.len = base.tonumber(s) * 24*3600*1000 
   end 
   
   s =  str:match("catchup%-minutes=[\"\'](%d+)[\"\']") or str:match("catchup%-minutes=(%d+)")
   if s then
     t.len = base.tonumber(s) * 60*1000 
   end 
   
   s = str:match('catchup%-source=[\"\'](.-)[\"\']')
   if s then
    t.source = trim(s)
	if     t.source 
	   and (t.catchupType == nil or t.catchupType == 'none' or t.catchupType == '') 
	 then
	  t.catchupType = 'default'
	end
   end
   
   s = str:match('catchup%-record%-source=[\"\'](.-)[\"\']')
   if s then
    t.recordSource = trim(s)
   end
   
 end
  return t
end
----------------------------------------------------------------------
local function dispatchRawM3UString(str)
    
 local t = {}
 t.catchupType = 'none'
 t.len  = 3*24*3600*1000  --default 3 days
 
 if base.type(str)=='string' then
 
  local localStr  = str
  local globalStr
  if str:match('#EXTM3U') then
    localStr =  str:match('(.-)#EXTM3U')  
    globalStr = str:match('#EXTM3U(.+)') 
  end
 
  Log ('-----------------------------------------')
  Log ('localStr:' .. (localStr or 'undefined') )
  Log ('globalStr:' .. (globalStr or 'undefined') )
   
  t = dispatchOneString(t,globalStr)
  t = dispatchOneString(t,localStr)
 
  Log ('-----------------------------------------')
  Log ('catchupParams:')
  Log ('  catchupType:' .. t.catchupType)
  Log ('  len:'         .. t.len)
  Log ('  source:'      .. (t.source or 'undefined'))
  Log ('  recordSource:'.. (t.recordSource or 'undefined') )
  
 end
 return t
end
----------------------------------------------------------------------  	
local function isSupportedCatchup(catchupParams)
 
 if  catchupParams.len <= 0 then return false end
 
 return     catchupParams.catchupType == 'default'
        or  catchupParams.catchupType == "append"
        or  catchupParams.catchupType == 'shift'
        or  catchupParams.catchupType == 'flussonic'
        or  catchupParams.catchupType == 'flussonic-hls'
        or  catchupParams.catchupType == 'fs'
        --[[TODO
        or  catchupParams.catchupType == 'flussonic-ts'                
        or  catchupParams.catchupType == 'stalker']]
end
----------------------------------------------------------------------
--Address build
----------------------------------------------------------------------
local function makeVars(offset,length)
 local t = {}
 t.timestamp = base.os.time() 
 t.offset    = offset
 t.start     = t.timestamp - offset
 if length~=nil and length > 0 then
    t.length  = length
    t.endTime = t.start + length
 end
 
 Log ('-----------------------------------------')
 Log ('Vars')
 Log (' timestamp:' .. t.timestamp)
 Log (' start:'     .. t.start)
 Log (' offset:'    .. t.offset)
 Log (' length:'    .. (t.length or 'undefined') )
 Log (' endTime:'   .. (t.endTime or 'undefined') )
  
 return t
end
----------------------------------------------------------------------
local function parseVars(address,vars)
 
 address = address:gsub('%${start}',vars.start)
 address = address:gsub('%${timestamp}',vars.timestamp)
 address = address:gsub('%${offset}',vars.offset)
   
 if vars.length then 
   address = address:gsub('%${length}',vars.length)
   address = address:gsub('%${duration}',vars.length)
 else 
   address = address:gsub('%${duration}', "86400")
 end  
   
 if vars.endTime then 
   address = address:gsub('%${end}',vars.endTime)
 end  
  
 return address
end
---------------------------------------------------------------------- 
local function dispathDefault(catchupParams,inAddress,offset,length)
 local vars = makeVars(offset,length)
 local outAddress = inAddress
 
 if    vars.length~=nil 
   and catchupParams.recordSource~=nil 
   and catchupParams.recordSource~='' then 
      outAddress = catchupParams.recordSource
 elseif vars.offset > 0 and catchupParams.source~=nil and catchupParams.source~='' then
     outAddress = catchupParams.source
 end
 
 return parseVars(outAddress,vars)
end
----------------------------------------------------------------------  	
local  function dispathAppend(catchupParams,inAddress,offset,length)
 local vars = makeVars(offset,length)
 local outAddress = parseVars(inAddress,vars)
  
 local appendStr  
  
 if     vars.length~=nil 
   and  catchupParams.recordSource~=nil 
   and  catchupParams.recordSource~='' then 
   appendStr  = parseVars(catchupParams.recordSource,vars)   
 elseif vars.offset > 0 and catchupParams.source~=nil and catchupParams.source~='' then
    appendStr  = parseVars(catchupParams.source,vars)
 end
 
 if appendStr then
   if outAddress:match('%?')
      and appendStr:match('%?') then
      outAddress = outAddress .. appendStr:gsub('%?','&')
   else 
   outAddress = outAddress .. appendStr
   end
 end
 
 return outAddress
end
----------------------------------------------------------------------  	
local function dispathShift(catchupParams,inAddress,offset,length)
   
 local vars = makeVars(offset,length)
 local outAddress = inAddress
 if vars.offset > 0 then
    if catchupParams.source~=nil and catchupParams.source~='' then
       outAddress = catchupParams.source
    end
    outAddress = outAddress .. '?utc=' .. vars.start .. '&lutc=' .. vars.timestamp 
 end
  
 return outAddress
end
----------------------------------------------------------------------  	
local function dispathFlussonicHls(catchupParams,inAddress,offset,length)
 
 if offset == 0 then return inAddress end
 
 local vars = makeVars(offset,length)
 
 local addressParams  
 local nakedAddress  =  inAddress
 if inAddress:match('%?') then 
    addressParams = base.string.match(inAddress,'?(.+)')
    nakedAddress  = base.string.match(inAddress,'(.-)?')
 end    
 
 local address1,address2  =  nakedAddress:match('(.+)/(.+)$')
 if address2 then 
   address2 = address2:match('(.-)%.')
   if    address2=='index'
      or address2=='playlist'
   then 
     address2=''
   end
 end
 address2 = address2 or ''
  
 if vars.length~=nil then 
   --record mode
   if address2 == '' then  
      address2 = 'archive-' .. vars.start .. '-' .. vars.length --archive-1508403742-3600.m3u8
   else 
    address2 = address2 .. '-' .. vars.start .. '-' .. vars.length --video-1508403742-3600.m3u8
   end    
 else
  --playback mode
  if address2 == '' then 
   address2 = 'timeshift_rel-' .. vars.offset --timeshift_rel-3600.m3u8
  else 
   address2 = address2 .. '-timeshift_rel-' .. vars.offset --video-timeshift_rel-3600.m3u8
  end
 end
 
 Log('fs nakedAddress:' .. (nakedAddress or 'undefined'))
 Log('fs addressParams:' .. (addressParams or 'undefined'))
 Log('fs address1:' .. address1)
 Log('fs address2:' .. address2)
 
 local  outAddress = address1 .. '/' .. address2 .. '.m3u8' 
 if addressParams then outAddress = outAddress .. '?' .. addressParams end
 
 return outAddress
end
----------------------------------------------------------------------  	
local function dispathFlussonicTs(catchupParams,inAddress,offset,length)
--TODO
end
----------------------------------------------------------------------
local function dispathStalker(catchupParams,inAddress,offset,length)
--TODO
end
---------------------------------------------------------------------- 
local function getPlayAddress(catchupParams,inAddress,offset,length)
 if catchupParams.catchupType == 'default' then
   return dispathDefault(catchupParams,inAddress,offset,length)
 end
 
 if catchupParams.catchupType == 'append' then
   return dispathAppend(catchupParams,inAddress,offset,length)
 end
 
 if catchupParams.catchupType == 'shift' then
   return dispathShift(catchupParams,inAddress,offset,length)
 end
 
 if   catchupParams.catchupType == 'flussonic' 
   or catchupParams.catchupType == 'flussonic-hls'  
   or catchupParams.catchupType == 'fs'  
  then
   return dispathFlussonicHls(catchupParams,inAddress,offset,length)
 end

 if catchupParams.catchupType == 'flussonic-ts' then
   return dispathFlussonicTs(catchupParams,inAddress,offset,length)
 end
   
 if catchupParams.catchupType == 'stalker' then
   return dispathStalker(catchupParams,inAddress,offset,length)
 end
end
----------------------------------------------------------------------  	
--Ext Events
----------------------------------------------------------------------  	
function addEventExecutor(functionName)
 if base.type(functionName)~='string' or functionName=='' then 
  Log ('addEventExecutor: bad functionName',1)
  return 
 end
 for i=1,#eventExecutorTable do
   if eventExecutorTable[i] == functionName then return end     
 end  
 base.table.insert(eventExecutorTable,functionName)
end
----------------------------------------------------------------------  	
function removeEventExecutor(functionName)
 if base.type(functionName)~='string' or functionName=='' then 
  Log ('removeEventExecutor: bad functionName',1)
  return 
 end
 for i=1,#eventExecutorTable do
   if eventExecutorTable[i] == functionName then 
     base.table.remove(eventExecutorTable,i)
     return
   end     
 end
end
----------------------------------------------------------------------  	
local function doEvent(eventType, eventParams )   
 for i=1,#eventExecutorTable do
   if base[eventExecutorTable[i]](eventType, eventParams) then
      break     
   end     
 end
end 
----------------------------------------------------------------------  	
--Main handler
----------------------------------------------------------------------  	
function handler(inQueryType, inParams)

  if      base.package.loaded['tvs_core'] 
     and base.tvs_core.dispatchTimeshift ~= nil
    then
     inParams = base.tvs_core.dispatchTimeshift(inQueryType,inParams)
  end
  
  local eventParams ={}
  eventParams.queryType = inQueryType
  eventParams.params    = inParams
  
  doEvent('StartProcessing', eventParams )
  Log ('-----------------------------------------')
  Log ('queryType:' .. eventParams.queryType)
  if eventParams.params then
    Log(' channelId:'    .. (eventParams.params.channelId or 'undefined'))
    Log(' address:'      .. (eventParams.params.address or 'undefined'))
    Log(' offset:'       .. (eventParams.params.offset or 'undefined'))
    Log(' length:'       .. (eventParams.params.length or 'undefined'))
    Log(' epgId:'        .. (eventParams.params.epgId or 'undefined'))
    Log(' rawM3UString:' .. (eventParams.params.rawM3UString or 'undefined'))
    Log(' position:'     .. (eventParams.params.position or 'undefined'))
  end
  Log ('-----------------------------------------')
   
 if   eventParams.stopProccessing then return end
 ----------------------------------------------------------------------  	
  if eventParams.queryType == 'GetLengthByAddress' then
    eventParams.catchupParams = dispatchRawM3UString(eventParams.params.rawM3UString)
    if isSupportedCatchup(eventParams.catchupParams) then 
       return eventParams.catchupParams.len
    end
   return
  end
----------------------------------------------------------------------   
  if eventParams.queryType == 'TestAddress' then
	 reset()
	 eventParams.catchupParams = dispatchRawM3UString(eventParams.params.rawM3UString)
     return  isSupportedCatchup(eventParams.catchupParams)
  end
----------------------------------------------------------------------  
  if eventParams.queryType == 'Start' then
		
	reset()
    
    eventParams.catchupParams = dispatchRawM3UString(eventParams.params.rawM3UString)
    if not isSupportedCatchup(eventParams.catchupParams) then return end
        
    eventParams.extopt    = base.findpattern(eventParams.params.address,'%$OPT:(.+)',1,0,0) or ''        
    eventParams.inAddress = eventParams.params.address:gsub('%$OPT:(.+)', '')        
    if eventParams.params.offset > eventParams.catchupParams.len then 
       eventParams.params.offset = eventParams.catchupParams.len
    end
    
    local offset     = base.math.floor(eventParams.params.offset / 1000)
    
    doEvent('PreTransformPlayAddress', eventParams )
    eventParams.outAddress = getPlayAddress(eventParams.catchupParams,eventParams.inAddress,offset)
    
    if eventParams.outAddress~=nil then
        currentData.Params        =  eventParams.params
        currentData.CatchupParams =  eventParams.catchupParams
        currentData.currentOffset =  offset
        
        currentData.currentPosition =  1 - ((currentData.currentOffset*1000) / currentData.CatchupParams.len)
        
        eventParams.outAddress = eventParams.outAddress .. eventParams.extopt
    end
    
    doEvent('PostTransformPlayAddress', eventParams )
    Log ('outAddress:' .. (eventParams.outAddress or 'undefined'))
    
    return eventParams.outAddress
  end
----------------------------------------------------------------------  
  if eventParams.queryType == 'IsSeekable' then
	 return true
  end
----------------------------------------------------------------------   
 if eventParams.queryType == 'GetPosition' then
   if currentData.Params == nil then return end
   
   if currentData.pauseStart then      
     currentData.currentPosition = 1 - (((currentData.currentOffset + base.os.time() - currentData.pauseStart) * 1000) / currentData.CatchupParams.len)
     if currentData.currentPosition < 0 then
  	     currentData.currentPosition = 0
     end
   end
  return currentData.currentPosition
 end
----------------------------------------------------------------------  
 if eventParams.queryType == 'GetLength' then
	if currentData.Params == nil then return end
    
    if currentData.CatchupParams then
      return currentData.CatchupParams.len
    end
   return
 end
----------------------------------------------------------------------    
 if eventParams.queryType == 'SetPosition'  then     
     stopTimer()
     if currentData.Params == nil then return end     
     
     if not currentData.currentPosition then return false end
	 currentData.currentPosition = eventParams.params.position
	 currentData.TimerId = base.m_simpleTV.Timer.SetTimer(1500, 'httpTimeshift.setDelayedPosition()',nil,true)
   return true
 end
----------------------------------------------------------------------          
 if eventParams.queryType == 'OnStop' then
    if currentData.Params == nil then return end
    reset()
    return
 end
----------------------------------------------------------------------          
 if eventParams.queryType == 'OnPaused' then
  	if currentData.Params == nil then return end     
    
    currentData.pauseStart = base.os.time()
   return
 end
----------------------------------------------------------------------                  
 if eventParams.queryType == 'OnPlaying' then
    if currentData.Params == nil then return end     
    
    if currentData.pauseStart then      
	  local len = base.os.time() - currentData.pauseStart      
      currentData.pauseStart = nil
	  currentData.currentOffset = currentData.currentOffset + len
      if currentData.currentOffset > currentData.CatchupParams.len then
         currentData.currentOffset = currentData.CatchupParams.len
      end
	  if len > 40 then
		 local offset = currentData.currentOffset + 5
         if offset > currentData.CatchupParams.len then
            offset = currentData.CatchupParams.len
         end
         reset()
		 base.m_simpleTV.Control.SetNewAddressT({timeshiftOffset = offset*1000})	  
      else 
       currentData.currentPosition =  1 - ((currentData.currentOffset*1000) / currentData.CatchupParams.len)
      end      
    end
  return
  end
----------------------------------------------------------------------          
  if eventParams.queryType == 'IsRecordAble' then
    eventParams.catchupParams = dispatchRawM3UString(eventParams.params.rawM3UString)
    return  isSupportedCatchup(eventParams.catchupParams) and eventParams.params.offset <= eventParams.catchupParams.len
  end
----------------------------------------------------------------------          
 if eventParams.queryType == 'GetRecordAddress' then
    eventParams.catchupParams = dispatchRawM3UString(eventParams.params.rawM3UString)
    if not isSupportedCatchup(eventParams.catchupParams) then return end
    if eventParams.params.offset > eventParams.catchupParams.len then return  end
    
    eventParams.extopt    = base.findpattern(eventParams.params.address,'%$OPT:(.+)',1,0,0) or ''        
    eventParams.inAddress = eventParams.params.address:gsub('%$OPT:(.+)', '')        
    local offset    = base.math.floor(eventParams.params.offset / 1000) + startRecordMargin()*60
    local length    = base.math.floor((eventParams.params.length or 0) / 1000)  
    if length > 0 then
     length = length + startRecordMargin()*60 + endRecordMargin()*60
    end
    
    doEvent('PreTransformRecordAddress', eventParams )
    eventParams.recordAddress = getPlayAddress(eventParams.catchupParams,eventParams.inAddress,offset,length)
    
    if eventParams.recordAddress~=nil then
        eventParams.recordAddress = eventParams.recordAddress .. eventParams.extopt
    end
    
    doEvent('PostTransformRecordAddress', eventParams )
    Log ('recordAddress:' .. (eventParams.recordAddress or 'undefined'))
   	   
   return eventParams.recordAddress
  end
  ----------------------------------------------------------------------                
 if eventParams.queryType == 'IsDisableRateChange' then
    if currentData.Params == nil then return end     
    
    return true
 end
        
end


