require 'torch'
require 'nn'
require 'optim'
require 'image'

require 'fast_neural_style.PerceptualCriterion'
require 'fast_neural_style.TotalVariation'
local utils = require 'fast_neural_style.utils'
local preprocess = require 'fast_neural_style.preprocess'

--[[
Perform optimization-based style transfer as described in
--]]

local cmd = torch.CmdLine()

-- Basic options
cmd:option('-content_image', 'images/content/chicago.jpg')
cmd:option('-style_image', 'images/styles/starry_night.jpg')
cmd:option('-image_size', 512)

-- Loss options
cmd:option('-loss_network', 'models/vgg16.t7')
cmd:option('-tv_strength', 1e-6)
cmd:option('-loss_type', 'L2', 'L2|SmoothL1')
cmd:option('-style_target_type', 'gram', 'gram|mean')

-- Options for content reconstruction
cmd:option('-content_weights', '1.0')
cmd:option('-content_layers', '16')

-- Options for style reconstruction
cmd:option('-style_weights', '5.0')
cmd:option('-style_layers', '4,9,16,23')
cmd:option('-style_image_size', 512)

-- Options for DeepDream
cmd:option('-deepdream_layers', '')
cmd:option('-deepdream_weights', '')

-- Optimization
cmd:option('-learning_rate', 1.0)
cmd:option('-optimizer', 'lbfgs', 'lbfgs|adam')
cmd:option('-num_iterations', 500)

-- Output options
cmd:option('-output_image', 'out.png')
cmd:option('-print_every', 1)
cmd:option('-save_every', 50)

-- Other options
cmd:option('-preprocessing', 'vgg')

-- Backend options
cmd:option('-gpu', -1)
cmd:option('-backend', 'cuda', 'cuda|opencl')
cmd:option('-use_cudnn', 1)


local opt = cmd:parse(arg)


local function main()
  if not preprocess[opt.preprocessing] then
    local msg = 'invalid -preprocessing "%s"; must be "vgg" or "resnet"'
    error(string.format(msg, opt.preprocessing))
  end
  preprocess = preprocess[opt.preprocessing]
  
  local dtype, use_cudnn = utils.setup_gpu(opt.gpu, opt.backend, opt.use_cudnn == 1)
  
  -- Set up the criterion
  local ok, loss_net = pcall(function() return torch.load(opt.loss_network) end)
  if not ok then
    print('ERROR: Could not load loss network from ' .. opt.loss_network)
    print('You may need to download the VGG-16 model by running:')
    print('bash models/download_vgg16.sh')
    return
  end
  print(loss_net)
  local style_layers, style_weights =
    utils.parse_layers(opt.style_layers, opt.style_weights)
  local content_layers, content_weights =
    utils.parse_layers(opt.content_layers, opt.content_weights)
  local deepdream_layers, deepdream_weights =
    utils.parse_layers(opt.deepdream_layers, opt.deepdream_weights)
  local crit_args = {
    cnn = loss_net,
    style_layers = style_layers,
    style_weights = style_weights,
    content_layers = content_layers,
    content_weights = content_weights,
    deepdream_layers = deepdream_layers,
    deepdream_weights = deepdream_weights,
    loss_type = opt.loss_type,
    agg_type = opt.style_target_type,
  }
  local crit = nn.PerceptualCriterion(crit_args):type(dtype)
  
  -- Set the content image
  local content_image = image.load(opt.content_image, 3)
  content_image = image.scale(content_image, opt.image_size)
  local H, W = content_image:size(2), content_image:size(3)
  content_image = preprocess.preprocess(content_image:view(1, 3, H, W))
  crit:setContentTarget(content_image:type(dtype))
  
  -- Set the style image
  local style_image = image.load(opt.style_image, 3)
  style_image = image.scale(style_image, opt.style_image_size)
  local H, W = style_image:size(2), style_image:size(3)
  style_image = preprocess.preprocess(style_image:view(1, 3, H, W))
  crit:setStyleTarget(style_image:type(dtype))

  -- Set up total variation regularization
  local tv = nn.Identity()
  if opt.tv_strength > 0 then
    tv = nn.TotalVariation(opt.tv_strength)
  end
  tv:type(dtype)

  local img = torch.randn(#content_image):type(dtype)
  
  -- Callback function for optim methods
  local f_calls = 0
  local function f(x)
    f_calls = f_calls + 1
    local tv_out = tv:forward(x)
    local loss = crit:forward(tv_out, {})
    local grad_tv_out = crit:updateGradInput(tv_out, {})
    local grad_x = tv:backward(x, grad_tv_out)
 
    if opt.print_every > 0 and f_calls % opt.print_every == 0 then
      print(string.format('Iteration %d, loss = %f', f_calls, loss))
    end
 
    if opt.save_every > 0 and f_calls % opt.save_every == 0 then
      local img_out = preprocess.deprocess(img:float())[1]
      local ext = paths.extname(opt.output_image)
      local basename = paths.basename(opt.output_image):split('%.')[1]
      local directory = paths.dirname(opt.output_image)
      local filename = string.format('%s/%s_%d.%s',
                          directory, basename, f_calls, ext)
      image.save(filename, img_out)
    end
    
    return loss, grad_x:view(-1)
  end

  if opt.optimizer == 'lbfgs' then
    local config = {
      maxIter=opt.num_iterations,
      learningRate=opt.learning_rate,
      verbose=true,
      tolX = 0,
    }
    optim.lbfgs(f, img, config)
  else
    local config = {
      learningRate = opt.learning_rate
    }
    local optim_fn = optim[opt.optimizer]
    if optim_fn == nil then
      error(string.format('Invalid optimizer "%s"', opt.optimizer))
    end
    for t = 1, opt.num_iterations do
      optim_fn(f, img, config)
    end
  end
end


main()
