-- Generic utility functions

module( ..., package.seeall )

local lfs = require "lfs"
local sf = string.format
local md5 = require "md5"

-- Taken from Lake
dir_sep = package.config:sub( 1, 1 )
is_os_windows = dir_sep == '\\'

-- Converts a string with items separated by 'sep' into a table
string_to_table = function( s, sep )
  if type( s ) ~= "string" then return end
  sep = sep or ' '
  if s:sub( -1, -1 ) ~= sep then s = s .. sep end
  s = s:gsub( sf( "^%s*", sep ), "" )
  local t = {}
  local fmt = sf( "(.-)%s+", sep )
  for w in s:gmatch( fmt ) do table.insert( t, w ) end
  return t
end

-- Split a file name into 'path part' and 'extension part'
split_ext = function( s )
  local pos
  for i = #s, 1, -1 do
    if s:sub( i, i ) == "." then
      pos = i
      break
    end
  end
  if not pos or s:find( dir_sep, pos + 1 ) then return s end
  return s:sub( 1, pos - 1 ), s:sub( pos )
end

-- Replace the extension of a given file name
replace_extension = function( s, newext )
  local p, e = split_ext( s )
  if e then 
    if newext and #newext > 0 then 
      s = p .. "." .. newext
    else
      s = p
    end
  end
  return s
end

-- Return 'true' if building from Windows, false otherwise
is_windows = function()
  return is_os_windows
end

-- Prepend each component of a 'pat'-separated string with 'prefix'
prepend_string = function( s, prefix, pat )  
  if not s or #s == 0 then return "" end
  pat = pat or ' '
  local res = ''
  local st = string_to_table( s, pat )
  foreach( st, function( k, v ) res = res .. prefix .. v .. " " end )
  return res
end

-- Like above, but consider 'prefix' a path
prepend_path = function( s, prefix, pat )
  return prepend_string( s, prefix .. dir_sep, pat )
end

-- full mkdir: create all the paths needed for a multipath
full_mkdir = function( path )
  local ptables = string_to_table( path, dir_sep )
  local p, res = ''
  for i = 1, #ptables do
    p = ( i ~= 1 and p .. dir_sep or p ) .. ptables[ i ]
    res = lfs.mkdir( p )
  end
  return res
end

-- Concatenate the given paths to form a complete path
concat_path = function( paths )
  return table.concat( paths, dir_sep )
end

-- Return true if the given array contains the given element, false otherwise
array_element_index = function( arr, element )
  for i = 1, #arr do
    if arr[ i ] == element then return i end
  end
end

-- Linearize an array with (possibly) embedded arrays into a simple array
_linearize_array = function( arr, res, filter )
  if type( arr ) ~= "table" then return end
  for i = 1, #arr do
    local e = arr[ i ]
    if type( e ) == 'table' and filter( e ) then
      _linearize_array( e, res, filter )
    else
      table.insert( res, e )
    end
  end 
end

linearize_array = function( arr, filter )
  local res = {}
  filter = filter or function( v ) return true end
  _linearize_array( arr, res, filter )
  return res
end

-- Return an array with the keys of a table
table_keys = function( t )
  local keys = {}
  foreach( t, function( k, v ) table.insert( keys, k ) end )
  return keys
end

-- Return an array with the values of a table
table_values = function( t )
  local vals = {}
  foreach( t, function( k, v ) table.insert( vals, v ) end )
  return vals
end

-- Returns true if 'path' is a regular file, false otherwise
is_file = function( path )
  return lfs.attributes( path, "mode" ) == "file"
end

-- Returns true if 'path' is a directory, false otherwise
is_dir = function( path )
  return lfs.attributes( path, "mode" ) == "directory"
end

-- Return a list of files in the given directory matching a given mask
get_files = function( path, mask, norec, level )
  local t = ''
  level = level or 0
  for f in lfs.dir( path ) do
    local fname = path .. dir_sep .. f
    if lfs.attributes( fname, "mode" ) == "file" then
      local include
      if type( mask ) == "string" then
        include = fname:find( mask )
      else
        include = mask( fname )
      end
      if include then t = t .. ' ' .. fname end
    elseif lfs.attributes( fname, "mode" ) == "directory" and not fname:find( "%.+$" ) and not norec then
      t = t .. " " .. get_files( fname, mask, norec, level + 1 )
    end
  end
  return level > 0 and t or t:gsub( "^%s+", "" )
end

-- Check if the given command can be executed properly
check_command = function( cmd )
  local res = os.execute( cmd .. " > .build.temp 2>&1" )
  os.remove( ".build.temp" )
  return res
end

-- Execute a command and capture output
-- From: http://stackoverflow.com/a/326715/105950
exec_capture = function( cmd, raw )
  local f = assert(io.popen(cmd, 'r'))
  local s = assert(f:read('*a'))
   f:close()
   if raw then return s end
   s = string.gsub(s, '^%s+', '')
   s = string.gsub(s, '%s+$', '')
   s = string.gsub(s, '[\n\r]+', ' ')
   return s
end

-- Execute the given command for each value in a table
foreach = function ( t, cmd )
  if type( t ) ~= "table" then return end
  for k, v in pairs( t ) do cmd( k, v ) end
end

-- Generate header with the given #defines, return result as string
gen_header_string = function( name, defines )
  local s = "// eLua " .. name:lower() .. " definition\n\n"
  s = s .. "#ifndef __" .. name:upper() .. "_H__\n"
  s = s .. "#define __" .. name:upper() .. "_H__\n\n"

  for key,value in pairs(defines) do 
     s = s .. string.format("#define   %-25s%-19s\n",key:upper(),value)
  end

  s = s .. "\n#endif\n"
  return s
end

-- Generate header with the given #defines, save result to file
gen_header_file = function( name, defines )
  local hname = concat_path{ "inc", name:lower() .. ".h" }
  local h = assert( io.open( hname, "w" ) )
  h:write( gen_header_string( name, defines ) )
  h:close()
end

-- Remove the given elements from an array
remove_array_elements = function( arr, del )
  del = istable( del ) and del or { del }
  foreach( del, function( k, v )
    local pos = array_element_index( arr, v )
    if pos then table.remove( arr, pos ) end
  end )
end

-- Remove a directory recusively
-- USE WITH CARE!! Doesn't do much checks :)
rmdir_rec = function ( dirname )
  if lfs.attributes( dirname, "mode" ) ~= "directory" then return end
  for f in lfs.dir( dirname ) do
    local ename = string.format( "%s/%s", dirname, f )
    local attrs = lfs.attributes( ename )
    if attrs.mode == 'directory' and f ~= '.' and f ~= '..' then
      rmdir_rec( ename ) 
    elseif attrs.mode == 'file' or attrs.mode == 'named pipe' or attrs.mode == 'link' then
      os.remove( ename )
    end
  end
  lfs.rmdir( dirname )
end

-- Computes the hash of the given string
get_hash_of_string = function( s )
  return md5.sumhexa( s )
end

-- Computes the hash of the given file
get_hash_of_file = function( f )
  local f = io.open( f, "rb" )
  if not f then return end
  local d = f:read( "*a" )
  f:close()
  return get_hash_of_string( d )
end

-- Concatenates the second table into the first one
concat_tables = function( dst, src )
  foreach( src, function( k, v ) dst[ k ] = v end )
end

-------------------------------------------------------------------------------
-- Color-related funtions
-- Currently disabled when running in Windows
-- (they can be enabled by setting WIN_ANSI_TERM)

local dcoltable = { 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' }
local coltable = {}
foreach( dcoltable, function( k, v ) coltable[ v ] = k - 1 end )

local _col_builder = function( col )
  local _col_maker = function( s )
    if is_os_windows and not os.getenv( "WIN_ANSI_TERM" ) then
      return s
    else
      return( sf( "\027[%d;1m%s\027[m", coltable[ col ] + 30, s ) )
    end
  end
  return _col_maker
end

col_funcs = {}
foreach( coltable, function( k, v ) 
  local fname = "col_" .. k
  _G[ fname ] = _col_builder( k ) 
  col_funcs[ k ] = _G[ fname ]
end )

-------------------------------------------------------------------------------
-- Option handling

local options = {}

options.new = function()
  local self = {}
  self.options = {}
  setmetatable( self, { __index = options } )
  return self
end

-- Argument validator: boolean value
options._bool_validator = function( v )
  if v == '0' or v:upper() == 'FALSE' then
    return false
  elseif v == '1' or v:upper() == 'TRUE' then
    return true
  end
end

-- Argument validator: choice value
options._choice_validator = function( v, allowed )
  for i = 1, #allowed do
    if v:upper() == allowed[ i ]:upper() then return allowed[ i ] end
  end
end

-- Argument validator: choice map (argument value maps to something)
options._choice_map_validator = function( v, allowed )
  for k, value in pairs( allowed ) do
    if v:upper() == k:upper() then return value end
  end
end

-- Argument validator: string value (no validation)
options._string_validator = function( v )
  return v
end

-- Argument printer: boolean value
options._bool_printer = function( o )
  return "true|false", o.default and "true" or "false"
end

-- Argument printer: choice value
options._choice_printer = function( o )
  local clist, opts  = '', o.data
  for i = 1, #opts do
    clist = clist .. ( i ~= 1 and "|" or "" ) .. opts[ i ]
  end
  return clist, o.default
end

-- Argument printer: choice map printer
options._choice_map_printer = function( o )
  local clist, opts, def = '', o.data
  local i = 1
  for k, v in pairs( opts ) do
    clist = clist .. ( i ~= 1 and "|" or "" ) .. k
    if o.default == v then def = k end
    i = i + 1
  end
  return clist, def
end

-- Argument printer: string printer
options._string_printer = function( o )
  return nil, o.default
end

-- Add an option of the specified type
options._add_option = function( self, optname, opttype, help, default, data )
  local validators = 
  { 
    string = options._string_validator, choice = options._choice_validator, 
    boolean = options._bool_validator, choice_map = options._choice_map_validator
  }
  local printers = 
  { 
    string = options._string_printer, choice = options._choice_printer, 
    boolean = options._bool_printer, choice_map = options._choice_map_printer
  }
  if not validators[ opttype ] then
    print( sf( "[builder] Invalid option type '%s'", opttype ) )
    os.exit( 1 )
  end
  table.insert( self.options, { name = optname, help = help, validator = validators[ opttype ], printer = printers[ opttype ], data = data, default = default } )
end

-- Find an option with the given name
options._find_option = function( self, optname )
  for i = 1, #self.options do
    local o = self.options[ i ]
    if o.name:upper() == optname:upper() then return self.options[ i ] end
  end
end

-- 'add option' helper (automatically detects option type)
options.add_option = function( self, name, help, default, data )
  local otype
  if type( default ) == 'boolean' then
    otype = 'boolean'
  elseif data and type( data ) == 'table' and #data == 0 then
    otype = 'choice_map'
  elseif data and type( data ) == 'table' then
    otype = 'choice'
    data = linearize_array( data )
  elseif type( default ) == 'string' then
    otype = 'string'
  else
    print( sf( "Error: cannot detect option type for '%s'", name ) )
    os.exit( 1 )
  end
  self:_add_option( name, otype, help, default, data )
end

options.get_num_opts = function( self )
  return #self.options
end

options.get_option = function( self, i )
  return self.options[ i ]
end

-- Handle an option of type 'key=value'
-- Returns both the key and the value or nil for error
options.handle_arg = function( self, a )
  local si, ei, k, v = a:find( "([^=]+)=(.*)$" )
  if not k or not v then 
    print( sf( "Error: invalid syntax in '%s'", a ) )
    return
  end
  local opt = self:_find_option( k )
  if not opt then
    print( sf( "Error: invalid option '%s'", k ) )
    return
  end
  local optv = opt.validator( v, opt.data )
  if optv == nil then
    print( sf( "Error: invalid value '%s' for option '%s'", v, k ) )
    return
  end
  return k, optv
end

-- Show help for all the registered options
options.show_help = function( self )
  for i = 1, #self.options do
    local o = self.options[ i ]
    print( sf( "\n  %s: %s", o.name, o.help ) )
    local values, default = o.printer( o )
    if values then
      print( sf( "    Possible values: %s", values ) )
    end
    print( sf( "    Default value: %s", default or "none (changes at runtime)" ) )
  end
end

-- Create a new option handler
function options_handler()
  return options.new()
end

