require 'torch'
require 'nn'
local cjson = require 'cjson'


local M = {}


-- Parse a string of comma-separated numbers
-- For example convert "1.0,3.14" to {1.0, 3.14}
function M.parse_num_list(s)
  local nums = {}
  for _, ss in ipairs(s:split(',')) do
    table.insert(nums, tonumber(ss))
  end
  return nums
end


-- Parse a layer string and associated weights string.
-- The layers string is a string of comma-separated layer strings, and the
-- weight string contains comma-separated numbers. If the weights string
-- contains only a single number it is duplicated to be the same length as the
-- layers.
function M.parse_layers(layers_string, weights_string)
  local layers = layers_string:split(',')
  local weights = M.parse_num_list(weights_string)
  if #weights == 1 and #layers > 1 then
    -- Duplicate the same weight for all layers
    local w = weights[1]
    weights = {}
    for i = 1, #layers do
      table.insert(weights, w)
    end
  elseif #weights ~= #layers then
    local msg = 'size mismatch between layers "%s" and weights "%s"'
    error(string.format(msg, layers_string, weights_string))
  end
  return layers, weights
end


function M.setup_gpu(gpu, backend, use_cudnn)
  local dtype = 'torch.FloatTensor'
  if gpu >= 0 then
    if backend == 'cuda' then
      require 'cutorch'
      require 'cunn'
      cutorch.setDevice(gpu + 1)
      dtype = 'torch.CudaTensor'
      if use_cudnn then
        require 'cudnn'
        cudnn.benchmark = true
      end
    elseif backend == 'opencl' then
      require 'cltorch'
      require 'clnn'
      cltorch.setDevice(gpu + 1)
      dtype = torch.Tensor():cl():type()
      use_cudnn = false
    end
  else
    use_cudnn = false
  end
  return dtype, use_cudnn
end


function M.clear_gradients(m)
  if torch.isTypeOf(m, nn.Container) then
    m:applyToModules(M.clear_gradients)
  end
  if m.weight and m.gradWeight then
    m.gradWeight = m.gradWeight.new()
  end
  if m.bias and m.gradBias then
    m.gradBias = m.gradBias.new()
  end
end


function M.restore_gradients(m)
  if torch.isTypeOf(m, nn.Container) then
    m:applyToModules(M.restore_gradients)
  end
  if m.weight and m.gradWeight then
    m.gradWeight = m.gradWeight.new(#m.weight):zero()
  end
  if m.bias and m.gradBias then
    m.gradBias = m.gradBias.new(#m.bias):zero()
  end
end


function M.read_json(path)
  local file = io.open(path, 'r')
  local text = file:read()
  file:close()
  local info = cjson.decode(text)
  return info
end


function M.write_json(path, j)
  cjson.encode_sparse_array(true, 2, 10)
  local text = cjson.encode(j)
  local file = io.open(path, 'w')
  file:write(text)
  file:close()
end

local IMAGE_EXTS = {'jpg', 'jpeg', 'png', 'ppm', 'pgm'}
function M.is_image_file(filename)
  -- Hidden file are not images
  if string.sub(filename, 1, 1) == '.' then
    return false
  end
  -- Check against a list of known image extensions
  local ext = string.lower(paths.extname(filename) or "")
  for _, image_ext in ipairs(IMAGE_EXTS) do
    if ext == image_ext then
      return true
    end
  end
  return false
end


function M.median_filter(img, r)
  local u = img:unfold(2, r, 1):contiguous()
  u = u:unfold(3, r, 1):contiguous()
  local HH, WW = u:size(2), u:size(3)
  local dtype = u:type()
  -- Median is not defined for CudaTensors, cast to float and back
  local med = u:view(3, HH, WW, r * r):float():median():type(dtype)
  return med[{{}, {}, {}, 1}]
end


return M

