---------------------------------------------------------------------------
-- @author Alexander Yakushev <yakushev.alex@gmail.com>
-- @copyright 2011-2013 Alexander Yakushev
-- @release v1.2.4
---------------------------------------------------------------------------

-- Grab environment
local awful = require('awful')

local jamendo = {}

-- UTILITY STUFF
-- Checks whether file specified by filename exists.
local function file_exists(filename, mode)
   mode = mode or 'r'
   f = io.open(filename, mode)
   if f then
      f:close()
      return true
   else
      return false
   end
end

local function str_interpose(coll, sep)
   if #coll == 0 then
      return ""
   end
   local result = coll[1]
   for i = 2, #coll do
      result = result .. sep .. coll[i]
   end
   print(result)
   return result
end

-- Global variables
jamendo.FORMAT_MP3 = { display = "MP3 (128k)",
                       short_display = "MP3",
                       value = "mp31" }
jamendo.FORMAT_OGG = { display = "Ogg Vorbis (q4)",
                       short_display = "Ogg",
                       value = "ogg2" }
jamendo.ORDER_RATINGDAILY = { display = "Daily rating",
                              short_display = "daily rating",
                              value = "ratingday_desc" }
jamendo.ORDER_RATINGWEEKLY = { display = "Weekly rating",
                               short_display = "weekly rating",
                               value = "ratingweek_desc" }
jamendo.ORDER_RATINGTOTAL = { display = "All time rating",
                              short_display = "all time rating",
                              value = "ratingtotal_desc" }
jamendo.ORDER_RANDOM = { display = "Random",
                         short_display = "random",
                         value = "random_desc" }
jamendo.ORDER_RELEVANCE = { display = "None (consecutive)",
                            short_display = "none",
                            value = "searchweight_desc" }
jamendo.SEARCH_ARTIST = { display = "Artist",
                          unit = "artist",
                          value = "artist_id" }
jamendo.SEARCH_ALBUM = { display = "Album",
                         unit = "album",
                         value = "album_id" }
jamendo.SEARCH_TAG = { display = "Tag",
                       unit = "tag",
                       value = "tag_id" }
jamendo.ALL_FORMATS = { jamendo.FORMAT_MP3, jamendo.FORMAT_OGG }
jamendo.ALL_ORDERS = { ORDER_RELEVANCE, ORDER_RANDOM, ORDER_RATINGDAILY,
                       ORDER_RATINGWEEKLY, ORDER_RATINGTOTAL }

jamendo.current_request_table = { unit = "track",
                                  fields = {"id", "artist_url", "artist_name", "name", 
                                            "stream", "album_image", "album_name" },
                                  joins = { "track_album", "album_artist" },
                                  params = { streamencoding = jamendo.FORMAT_MP3,
                                             order = jamendo.ORDER_RATINGWEEKLY,
                                             n = 100 }}

-- Local variables
local jamendo_list = {}
local cache_file = awful.util.getdir ("cache").."/jamendo_cache"
local cache_header = "[version=1.1.0]"
local album_covers_folder = awful.util.getdir("cache") .. "/jamendo_covers/"
local default_mp3_stream = nil
local search_template = { fields = { "id", "name" },
                          joins = {},
                          params = { order = ORDER_RELEVANCE,
                                     n = 1}}

-- DEPRECATED. Will be removed in the next major release.
-- Returns default stream number for MP3 format. Requests API for it
-- not more often than every hour.
local function get_default_mp3_stream()
   if not default_mp3_stream or 
      (os.time() - default_mp3_stream.last_checked) > 3600 then
      local trygetlink = 
         jamendo.perform_request("echo $(curl -w %{redirect_url} " .. 
                                 "'http://api.jamendo.com/get2/stream/track/redirect/" .. 
                                 "?streamencoding="..jamendo.FORMAT_MP3.value.."&id=729304')")
         local _, _, prefix = string.find(trygetlink, "stream(%d+)%.jamendo%.com")
         default_mp3_stream = { id = prefix, last_checked = os.time() }
   end
   return default_mp3_stream.id
end

-- Returns the track ID from the given link to Jamendo stream. If the
-- given text is not the Jamendo stream returns nil.
function jamendo.get_id_from_link(link)
   local _, _, id = string.find(link,"storage%-new.newjamendo.com/%?trackid=(%d+)")
   return id
end

-- Returns link to music stream for the given track ID. Uses MP3
-- format and the default stream for it.
local function get_link_by_id(id)
   -- This function is subject to change in the future.
   return string.format("http://storage-new.newjamendo.com?trackid=%s&format=mp31&u=0", id)
end

-- -- Returns the album id for given music stream.
-- function get_album_id_by_link(link)
--    local id = get_id_from_link(link, true)
--    if id and jamendo_list[id] then
--       return jamendo_list[id].album_id
--    end
-- end

-- Returns the track table for the given music stream.
function jamendo.get_track_by_link(link)
   local id = jamendo.get_id_from_link(link, true)
   if id and jamendo_list[id] then
      return jamendo_list[id]
   end
end

-- If a track is actually a Jamendo stream, replace it with normal
-- track name.
function jamendo.replace_link(track_name)
   local track = jamendo.get_track_by_link(track_name)
   if track then
      return track.display_name
   else
      return track_name
   end
end

-- Returns table of track IDs, names and other things based on the
-- request table.
function jamendo.return_track_table(request_table)
   local req_string = jamendo.form_request(request_table)
   local response = jamendo.perform_request(req_string)
   if not response then
      return nil -- Bad internet connection
   end
   local parse_table = jamendo.parse_json(response)
   for i = 1, #parse_table do
      if parse_table[i].stream == "" then
         -- Some songs don't have Ogg stream, use MP3 instead
         parse_table[i].stream = get_link_by_id(parse_table[i].id)
      end
      _, _, parse_table[i].artist_link_name = 
         string.find(parse_table[i].artist_url, "\\/artist\\/(.+)")
      -- Remove Jamendo escape slashes
      parse_table[i].artist_name =
         string.gsub(parse_table[i].artist_name, "\\/", "/")
      parse_table[i].name = string.gsub(parse_table[i].name, "\\/", "/")

      parse_table[i].display_name = 
         parse_table[i].artist_name .. " - " .. parse_table[i].name
      -- Do Jamendo a favor, extract album_id for the track yourself
      -- from album_image link :)
      local _, _, album_id = 
         string.find(parse_table[i].album_image, "\\/(%d+)\\/covers")
      parse_table[i].album_id = album_id or 0
      -- Save fetched tracks for further caching
      jamendo_list[parse_table[i].id] = parse_table[i]
   end
   jamendo.save_cache()
   return parse_table
end

-- Generates the request to Jamendo API based on provided request
-- table. If request_table is nil, uses current_request_table instead.
-- For all values that do not exist in request_table use ones from
-- current_request_table.
-- return - HTTP-request
function jamendo.form_request(request_table)
   local curl_str = "curl -A 'Mozilla/4.0' -fsm 5 \"%s\""
   local url = "http://api.jamendo.com/get2/%s/%s/json/%s/?%s"
   request_table = request_table or jamendo.current_request_table
   
   local fields = request_table.fields or jamendo.current_request_table.fields
   local joins = request_table.joins or jamendo.current_request_table.joins
   local unit = request_table.unit or jamendo.current_request_table.unit
   
   -- Form fields string (like field1+field2+fieldN)
   local f_string = str_interpose(fields, "+")
   -- Form joins string
   local j_string = str_interpose(joins, "+")

   local params = {}
   -- If parameters where supplied in request_table, add them to the
   -- parameters in current_request_table.
   if request_table.params and 
      request_table.params ~= jamendo.current_request_table.params then
      -- First fill params with current_request_table parameters
      for k, v in pairs(jamendo.current_request_table.params) do
         params[k] = v
      end
      -- Then add and overwrite them with request_table parameters
      for k, v in pairs(request_table.params) do
         params[k] = v
      end
   else -- Or just use current_request_table.params
      params = jamendo.current_request_table.params
   end
   -- Form parameter string (like param1=value1&param2=value2)
   local param_string = ""
   for k, v in pairs(params) do
      if type(v) == "table" then
         v = v.value
      end
      v = string.gsub(v, " ", "+")
      param_string = param_string .. "&" .. k .. "=" .. v
   end

   return string.format(curl_str, string.format(url, f_string, unit, j_string, param_string))
end

-- Primitive function for parsing Jamendo API JSON response.  Does not
-- support arrays. Supports only strings and numbers as values.
-- Provides basic safety (correctly handles special symbols like comma
-- and curly brackets inside strings)
-- text - JSON text
function jamendo.parse_json(text)
   local parse_table = {}
   local block = {}
   local i = 0
   local inblock = false
   local instring = false
   local curr_key = nil
   local curr_val = nil
   while i and i < string.len(text) do
      if not inblock then -- We are not inside the block, find next {
         i = string.find(text, "{", i+1)
         inblock = true
         block = {}
      else
         if not curr_key then -- We haven't found key yet
            if not instring then -- We are not in string, check for more tags
               local j = string.find(text, '"', i+1)
               local k = string.find(text, '}', i+1)
               if j and j < k then -- There are more tags in this block
                  i = j
                  instring = true
               else -- Block is over, we found its ending
                  i = k
                  inblock = false
                  table.insert(parse_table, block)
               end
            else -- We are in string, find its ending
               _, i, curr_key = string.find(text,'(.-[^%\\])"', i+1)
               instring = false
            end
         else -- We have the key, let's find the value
            if not curr_val then -- Value is not found yet
               if not instring then -- Not in string, check if value is string
                  local j = string.find(text, '"', i+1)
                  local k = string.find(text, '[,}]', i+1)
                  if j and j < k then -- Value is string
                     i = j
                     instring = true
                  else -- Value is int
                     _, i, curr_val = string.find(text,'(%d+)', i+1)
                  end
               else -- We are in string, find its ending
                  local j = string.find(text, '"', i+1)
                  if j == i+1 then -- String is empty
                     i = j
                     curr_val = ""
                  else
                     _, i, curr_val = string.find(text,'(.-[^%\\])"', i+1)
                     curr_val = jamendo.utf8_codes_to_symbols(curr_val)
                  end
                  instring = false
               end
            else -- We have both key and value, add it to table
               block[curr_key] = curr_val
               curr_key = nil
               curr_val = nil
            end
         end
      end
   end
   return parse_table
end

-- Jamendo returns Unicode symbols as \uXXXX. Lua does not transform
-- them into symbols so we need to do it ourselves.
function jamendo.utf8_codes_to_symbols (s)
   local hexnums = "[%dabcdefABCDEF]"
   local pattern = string.format("\\u(%s%s%s%s?)", 
                                 hexnums, hexnums, hexnums, hexnums)
   local decode = function(code)
                     code = tonumber(code, 16)
                     if code < 128 then -- one-byte symbol
                        return string.char(code)
                     elseif code < 2048 then -- two-byte symbol
                        -- Grab high and low bytes
                        local hi = math.floor(code / 64)
                        local lo = math.fmod(code, 64)
                        -- Return symbol as \hi\lo
                        return string.char(hi + 192, lo + 128)
                     elseif code < 65536 then
                        -- Grab high, middle and low bytes
                        local hi = math.floor(code / 4096)
                        local leftover = code - hi * 4096
                        local mi = math.floor(leftover / 64)
                        leftover = leftover - mi * 64
                        local lo = math.fmod(leftover, 64)
                        -- Return symbol as \hi\mi\lo
                        return string.char(hi + 224, mi + 160, lo + 128)
                     elseif code < 1114112 then
                        -- Grab high, highmiddle, lowmiddle and low bytes
                        local hi = math.floor(code / 262144)
                        local leftover = code - hi * 262144
                        local hm = math.floor(leftover / 4096)
                        leftover = leftover - hm * 4096
                        local lm = math.floor(leftover / 64)
                        local lo = math.fmod(leftover, 64)
                        -- Return symbol as \hi\hm\lm\lo
                        return string.char(hi + 240, hm + 128, lm + 128, lo + 128)
                     else -- It is not Unicode symbol at all
                        return tostring(code)
                     end
                  end
   return string.gsub(s, pattern, decode)
end

-- Retrieves mapping of track IDs to track names and album IDs to
-- avoid redundant queries when Awesome gets restarted.
local function retrieve_cache()
   local bus = io.open(cache_file)
   local track = {}
   if bus then
      local header = bus:read("*line")
      if header == cache_header then 
         for l in bus:lines() do
            local _, _, id, artist_link_name, album_name, album_id, track_name = 
               string.find(l,"(%d+)-([^-]+)-([^-]+)-(%d+)-(.+)")
            track = {}
            track.id = id
            track.artist_link_name = string.gsub(artist_link_name, '\\_', '-')
            track.album_name = string.gsub(album_name, '\\_', '-')
            track.album_id = album_id
            track.display_name = track_name
            jamendo_list[id] = track
         end
      else 
         -- We encountered an outdated version of the cache
         -- file. Let's just remove it.
         awful.util.spawn("rm -f " .. cache_file)
      end
   end
end

-- Saves track IDs to track names and album IDs mapping into the cache
-- file.
function jamendo.save_cache()
   local bus = io.open(cache_file, "w")
   bus:write(cache_header .. "\n")
   for id,track in pairs(jamendo_list) do
      bus:write(string.format("%s-%s-%s-%s-%s\n", id, 
                              string.gsub(track.artist_link_name, '-', '\\_'),
                              string.gsub(track.album_name, '-', '\\_'),
                              track.album_id, track.display_name))
   end
   bus:flush()
   bus:close()
end

-- Retrieve cache on initialization
retrieve_cache()

-- Returns a filename of the album cover and formed wget request that
-- downloads the album cover for the given track name. If the album
-- cover already exists returns nil as the second argument.
function jamendo.fetch_album_cover_request(track_id)
   local track = jamendo_list[track_id]
   local album_id = track.album_id

   if album_id == 0 then -- No cover for tracks without album!
      return nil
   end
   local file_path = album_covers_folder .. album_id .. ".jpg"

   if not file_exists(file_path) then -- We need to download it  
      -- First check if cache directory exists
      f = io.popen('test -d ' .. album_covers_folder .. ' && echo t')
      if f:read("*line") ~= 't' then
         awful.util.spawn("mkdir " .. album_covers_folder)
      end
      f:close()
      
      if not track.album_image then      -- Wow! We have album_id, but
         local a_id = tostring(album_id) --don't have album_image. Well,
         local prefix =                  --it happens.
            string.sub(a_id, 1, #a_id - 3)
         track.album_image = 
            string.format("http://imgjam.com/albums/s%s/%s/covers/1.100.jpg",
                          prefix == "" and 0 or prefix, a_id)
      end
      
      return file_path, string.format("wget %s -O %s 2> /dev/null",
                                      track.album_image, file_path)
   else -- Cover already downloaded, return its filename and nil
      return file_path, nil
   end
end

-- Returns a file containing an album cover for given track id.  First
-- searches in the cache folder. If file is not there, fetches it from
-- the Internet and saves into the cache folder.
function jamendo.get_album_cover(track_id)
   local file_path, fetch_req = jamendo.fetch_album_cover_request(track_id)
   if fetch_req then
      local f = io.popen(fetch_req)
      f:close()

      -- Let's check if file is finally there, just in case
      if not file_exists(file_path) then
         return nil
      end
   end
   return file_path
end

-- Same as get_album_cover, but downloads (if necessary) the cover
-- asynchronously.
function jamendo.get_album_cover_async(track_id)
   local file_path, fetch_req = jamendo.fetch_album_cover_request(track_id)
   if fetch_req then
      asyncshell.request(fetch_req)
   end
end

-- Checks if track_name is actually a link to Jamendo stream. If true
-- returns the file with album cover for the track.
function jamendo.try_get_cover(track_name)
   local id = jamendo.get_id_from_link(track_name)
   if id then
      return jamendo.get_album_cover(id)
   end
end

-- Same as try_get_cover, but calls get_album_cover_async inside.
function jamendo.try_get_cover_async(track_name)
   local id = jamendo.get_id_from_link(track_name)
   if id then
      return jamendo.get_album_cover_async(id)
   end
end

-- Returns the track table for given query and search method.
-- what - search method - SEARCH_ARTIST, ALBUM or TAG
-- s - string to search
function jamendo.search_by(what, s)
   -- Get a default request and set unit and query
   local req = search_template
   req.unit = what.unit
   req.params.searchquery = s
   local resp = jamendo.perform_request(jamendo.form_request(req))
   if resp then
      local search_res = jamendo.parse_json(resp)[1]
      
      if search_res then
         -- Now when we got the search result, find tracks filtered by
         -- this result.
         local params = {}
         params[what.value] = search_res.id
         req = { params = params }
         local track_table = jamendo.return_track_table(req)
         return { search_res = search_res, tracks = track_table }
      end
   end
end

-- Executes request_string with io.popen and returns the response.
function jamendo.perform_request(request_string)
   local bus = assert(io.popen(request_string,'r'))
   local response = bus:read("*all")
   bus:close()
   -- Curl with popen can sometimes fail to fetch data when the
   -- connection is slow. Let's try again if it fails.
   if #response == 0 then
      bus = assert(io.popen(request_string,'r'))
      response = bus:read("*all")
      bus:close()
      -- If it still can't read anything, return nil
      if #response ~= 0 then
         return nil
      end
   end
   return response
end

-- Sets default streamencoding in current_request_table.
function jamendo.set_current_format(format)
   jamendo.current_request_table.params.streamencoding = format
end

-- Sets default order in current_request_table.
function jamendo.set_current_order(order)
   jamendo.current_request_table.params.order = order
end

return jamendo
