local seed = 12345
torch.manualSeed(seed)

require 'Imports'

local cmd = GeneralOptions:get_flags()
local params = cmd:parse(arg)

if(params.profile == 1) then
   require 'Pepperfish'  
   profiler = new_profiler()
   profiler:start()
end

print(params)

local use_cuda = params.gpuid >= 0
params.use_cuda= use_cuda
if(use_cuda)then
    print('USING GPU '..params.gpuid)
    require 'cutorch'
    require 'cunn'
    cutorch.setDevice(params.gpuid + 1) 
    cutorch.manualSeed(seed)
    if(params.cudnn == 1) then
    	print('using cudnn')
		require 'cudnn'
	end
end


local load_problem
if(params.problem == "SequenceTagging") then
	load_problem = function(params)
		local problem_config = torch.load(params.problem_config)
		problem_config.batch_size = params.batch_size

		local y_shape = {problem_config.batch_size,problem_config.length,problem_config.domain_size} 
		problem_config.y_shape = y_shape
		local model = ChainSPEN(problem_config,params)
		local evaluator_factory =  function(batcher, soft_predictor)
			local hard_predictor = RoundingPredictor(soft_predictor,y_shape)
			return HammingEvaluator(batcher, function(x) return hard_predictor:predict(x) end)
		end 
		local preprocess_func = nil
		local train_batcher = BatcherFromFile(params.train_list, preprocess_func, params.batch_size, use_cuda)
		local test_batcher  = BatcherFromFile(params.test_list, preprocess_func, params.batch_size, use_cuda)
		return model, y_shape, evaluator_factory, preprocess_func, train_batcher, test_batcher
	end
elseif(params.problem == "MultiLabelClassification") then
	load_problem = function(params)
		local problem_config = torch.load(params.problem_config)
		problem_config.batch_size = params.batch_size

		local y_shape = {problem_config.batch_size,problem_config.label_dim,2} 
		problem_config.y_shape = y_shape
		local model = MLCSPEN(problem_config,params)
		local evaluator_factory =  function(batcher, soft_predictor)
			return MultiLabelEvaluation(batcher, soft_predictor, problem_config.prediction_thresh, params.results_file)
		end 

		local adder = nn.AddConstant(1)
		if(use_cuda) then adder:cuda() end

		preprocess_func = function(a,b,c) return adder:forward(a):clone(), b, c end --TODO: make this unnecessary by preprocessing data differently. Right now, the labels are 0-indexed, so we have to add one.
		
		local train_batcher = BatcherFromFile(params.train_list, preprocess_func, params.batch_size, use_cuda)
		local test_batcher  = BatcherFromFile(params.test_list, preprocess_func, params.batch_size, use_cuda)
		return model, y_shape, evaluator_factory, preprocess_func, train_batcher, test_batcher
	end
elseif(params.problem == "Denoise") then
	load_problem = function(params)
		local problem_config = torch.load(params.problem_config)
		problem_config.batch_size = params.batch_size
		
		local y_shape, train_preprocess_func 
		if(problem_config.use_random_crops == 1) then
			local crop_height = 96 --these are the crop sizes used in the proximalnet paper. TODO: surface command line options for these
			local crop_width = 128
			y_shape = {problem_config.batch_size,crop_height,crop_width}
			local a_crop_contiguous, b_crop_contiguous
			local y_crop_start_max = problem_config.height - crop_height
			local x_crop_start_max = problem_config.width  - crop_width

			--This randomly crops the images in order to speed up training
			--Note that it uses the same crop locations for every image in the minibatch
			train_preprocess_func = function(a,b,c)	
				local y_start = torch.rand(1):mul(y_crop_start_max):ceil()[1]
				local x_start = torch.rand(1):mul(x_crop_start_max):ceil()[1]
				local a_crop = a:narrow(2,y_start,crop_height):narrow(3,x_start,crop_width)
				local b_crop = b:narrow(2,y_start,crop_height):narrow(3,x_start,crop_width)
				a_crop_contiguous = a_crop_contiguous or a_crop:clone()
				a_crop_contiguous:copy(a_crop)

				b_crop_contiguous = b_crop_contiguous or b_crop:clone()
				b_crop_contiguous:copy(b_crop)

				return a_crop_contiguous, b_crop_contiguous, c
			end
		else
			y_shape = {problem_config.batch_size,problem_config.height,problem_config.width} 
		end

		problem_config.y_shape = y_shape
		local model = DenoiseSPEN(problem_config,params)
		local evaluator_factory =  function(batcher, soft_predictor)
			return PSNREvaluator(batcher, function(x) return soft_predictor:forward(x) end)
		end 


		local train_batcher = BatcherFromFile(params.train_list, train_preprocess_func, params.batch_size, use_cuda)

		--NOTE: this doesn't return the actual test set score, but an approximation using random crops on the dev set.
		--It will require some more engineering to be able to actually run on the full-size test images, as the network expects smaller images.
		local test_batcher  = BatcherFromFile(params.test_list, train_preprocess_func, params.batch_size, use_cuda)
		return model, y_shape, evaluator_factory, preprocess_func, train_batcher, test_batcher
	end
else
	error('invalid problem type')
end

local model, y_shape, evaluator_factory, preprocess_func, train_batcher, test_batcher = load_problem(params)

local pretrain_train_config = {}
do
	pretrain_train_config.soft_predictor = model.classifier_network
	pretrain_train_config.modules_to_update = model.classifier_network
	pretrain_train_config.stop_feature_backprop = false
	pretrain_train_config.stop_unary_backprop = false

	local criterion_name = (params.continuous_outputs == 1) and "MSECriterion" or "ClassNLLCriterion"
	print(params)

	pretrain_train_config.loss_wrapper = TrainingWrappers:independent_training(model.classifier_network, criterion_name, y_shape, params)
	pretrain_train_config.items_to_save = {
		classifier = model.classifier_network
	}
end

local full_train_config = {}
do 
	params.return_all_iterates = params.penalize_all_iterates == 1
	params.num_iterates = params.max_inference_iters
	local gd_inference_config = GradientBasedInferenceConfig:get_gd_inference_config(params)
	local full_gd_prediction_net = GradientBasedInference(y_shape, gd_inference_config):spen_inference(model)
	gd_prediction_net = full_gd_prediction_net
	if(params.return_all_iterates) then
		gd_prediction_net = nn.Sequential():add(full_gd_prediction_net):add(nn.SelectTable(1))
	end
	--initialize the unaries from a loaded model
	if(params.init_classifier ~= "") then
		assert(not (params.init_full_net ~= ""), "shouldn't be initializing both classifier and full energy network from file")
		print('initializing classifier from '..params.init_classifier)
		model.classifier_network:getParameters():copy(torch.load(params.init_classifier):getParameters())
	end
	if(params.init_full_net ~= "") then
		print('initializing parameters from '..params.init_full_net)
		if(params.use_cuda) then gd_prediction_net:double() end --the fact that we have to do this is mysterious. 
		gd_prediction_net:getParameters():copy(torch.load(params.init_full_net):getParameters())
		if(params.use_cuda) then gd_prediction_net:cuda() end
	end

	full_train_config.soft_predictor = gd_prediction_net
	full_train_config.modules_to_update = gd_prediction_net

	if(params.training_method == "E2E") then
		local criterion_name = (params.continuous_outputs == 1) and "MSECriterion" or "ClassNLLCriterion"
		full_train_config.loss_wrapper = TrainingWrappers:independent_training(full_gd_prediction_net, criterion_name, y_shape, params)
	elseif(params.training_method == "SSVM") then
		local criterion_name = 'MSECriterion' --todo: surface an option for this
		assert(not params.return_all_iterates)
		full_train_config.loss_wrapper = TrainingWrappers:ssvm_training(y_shape, model, criterion_name, gd_inference_config, params)
	else
	    error('invalid training method')
	end

	full_train_config.items_to_save = {
		predictor = gd_prediction_net,
		energy_net = model:full_energy_net()
	}
end

local clamp_features_train_config = Util:copyTable(full_train_config) --this is a copy by reference
clamp_features_train_config.stop_feature_backprop = true

local clamp_unaries_train_config = Util:copyTable(full_train_config) --this is a copy by reference
clamp_unaries_train_config.stop_unary_backprop = true
clamp_unaries_train_config.stop_feature_backprop = true


local train_configurations = {
	pretrain = pretrain_train_config,
	clamp_features = clamp_features_train_config,
	clamp_unaries = clamp_unaries_train_config,
	full = full_train_config
}


local function evaluate_only(config,params)
	if(params.use_cuda) then config.soft_predictor:cuda() end
	local evaluator = evaluator_factory(test_batcher, config.soft_predictor)
	evaluator:evaluate(0)
end

local function train(config, params, name)
	assert(config)

	if(params.use_cuda) then
		config.loss_wrapper:cuda()
		config.soft_predictor:cuda()
	end
	model:set_feature_backprop( not config.stop_feature_backprop )
	model:set_unary_backprop( not config.stop_unary_backprop )

	local callbacks = {}
	local evaluator = evaluator_factory(test_batcher, config.soft_predictor)
	local evaluate = Callback(function(data) return evaluator:evaluate(data.epoch) end, params.evaluation_frequency)
	table.insert(callbacks,evaluate)


	local opt_state = {}
	if(params.init_opt_state ~= "") then 
		print('loading opt_state from '..params.init_opt_state)
		opt_state = torch.load(params.init_opt_state)
	end
	local optimization_config = {
		opt_state = opt_state,
		opt_config = {
					  learningRate=params.learning_rate,
					  learningRateDecay=0, --this gets updated by lr_start below
					  beta1 = params.adam_beta1,
					  beta2 = params.adam_beta2,
					  epsilon=params.adam_epsilon,
					  weightDecay=params.l2
					 },
		opt_method = optim.adam,
		gradient_clip = params.gradient_clip,
		regularization = config.regularization,
		modules_to_update = config.modules_to_update
	}

	local general_config = {
		num_epochs = params.num_epochs,
		batches_per_epoch = params.batches_per_epoch,
		batch_size = params.batch_size,
		assert_nan = true,
	}

	local nonzero_learning_rate_set = false
	local function set_lr(data)
		if(data.epoch > params.learning_rate_decay_start and not set_nonzero_learning_rate) then
			optimization_config.opt_config.learningRateDecay = params.learning_rate_decay
			nonzero_learning_rate_set = true
		end
	end
	local lr_start = Callback(set_lr, 1)
	table.insert(callbacks,lr_start)

	if(params.icnn == 1) then
		local params_to_clamp = model.global_potentials_network:parameters()
		--todo: we actually don't need to clamp the biases
		local function clamp()
			Util:deep_apply(params_to_clamp,function(t) t:cmax(0) end)
		end
		general_config.post_process_parameter_update = clamp
	end

	config.items_to_save.opt_state = optimization_config.opt_state
	local saved_model_base = params.model_file.."-"..name
	local saver = Saver(saved_model_base,config.items_to_save)
	local save = Callback(function(data) return saver:save(data.epoch) end, params.save_frequency)
	table.insert(callbacks,save)

	Train(config.loss_wrapper,train_batcher, optimization_config, general_config, callbacks):train()
end

if(params.evaluate_classifier_only == 1) then
	evaluate_only(train_configurations.pretrain, params)
	os.exit()
end
if(params.evaluate_spen_only == 1) then
	evaluate_only(train_configurations.full, params)
	os.exit()
end

for params_file in io.lines(params.training_configs) do
	print('loading specific training config from '..params_file)
	local specific_params = torch.load(params_file)

	local mode = specific_params.training_mode
	if(specific_params.num_epochs > 0) then 
		print(specific_params)

		local all_params = Util:copyTable(params)
		for k,v in pairs(specific_params) do 
			assert(not all_params[k],'repeated key: '..k)
			all_params[k] = v 
		end

		print('starting training for mode: '..mode)
		if(mode == "pretrain_unaries") then
			train(train_configurations.pretrain, all_params, mode)
		elseif(mode == "clamp_unaries") then
			train(train_configurations.clamp_unaries, all_params, mode)
		elseif(mode == "clamp_features") then
			train(train_configurations.clamp_features, all_params, mode)
		elseif(mode == "update_all") then
			train(train_configurations.full, all_params, mode)
		else
			error('invalid training mode: '..mode)
		end
	end
end


if(params.profile == 1) then
	profiler:stop()
	local report = "profile.txt"
	print('writing profiling report to: '..report)
    local outfile = io.open( report, "w+" )
    profiler:report( outfile )
    outfile:close()
end



