/**
* Lubricant Detecter
* Copyright (C) 2021 Carl Klemm
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 3 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA  02110-1301, USA.
*/

#include <iostream>
#include <opencv2/highgui.hpp>
#include <opencv2/core/ocl.hpp>
#include <fcntl.h>
#include <unistd.h>
#include <math.h> 
#include <vector>
#include <string> 
#include "argpopt.h"
#include "uvosunwrap/unwrap.h"
#include "uvosunwrap/bgremoval.h"
#include "uvosunwrap/normalize.h"
#include "uvosunwrap/log.h"
#include "uvosunwrap/charuco.h"
#include "uvosunwrap/harris.h"
#include "uvosunwrap/curve.h"

#define IMREAD_SIZE pow(2, 20)

enum {
	CREATE_CHARUCO,
	CREATE_MAP,
	APPLY_MAP,
	APPLY_CURVE,
	CREATE_CURVE,
	EXIT
};

int selectOperation(char** opt)
{
	if(!opt || !opt[0])
		return -1;
	else if(strcmp(opt[0], "create" ) == 0)
		return CREATE_MAP;
	else if(strcmp(opt[0], "apply" ) == 0)
		return APPLY_MAP;
	else if(strcmp(opt[0], "curve" ) == 0)
		return APPLY_CURVE;
	else if(strcmp(opt[0], "mkcurve" ) == 0)
		return CREATE_CURVE;
	else if(strcmp(opt[0], "mkboard" ) == 0)
		return CREATE_CHARUCO;
	else if(strcmp(opt[0], "exit" ) == 0)
		return EXIT;
	return -1;
}

cv::Mat openImageImg(char* fileName)
{
	int fd = open(fileName, O_RDONLY);
	if(fd < 0)
	{
		Log(Log::WARN)<<"could not open "<<fileName<< "ignoreing";
		return cv::Mat();
	}
	
	size_t pos = 0;
	std::vector<char> buffer(IMREAD_SIZE);
	size_t count;
	while((count = read(fd, buffer.data()+pos, IMREAD_SIZE)) > 0)
	{
		pos+=count;
		buffer.resize(pos+IMREAD_SIZE);
		Log(Log::WARN)<<pos<<"  "<<IMREAD_SIZE;
	}
	close(fd);
	return cv::imdecode(buffer, cv::IMREAD_UNCHANGED);
}

cv::Mat openImageYml(char* fileName)
{
	cv::Mat image;
	cv::FileStorage matf(fileName, cv::FileStorage::READ);
	matf["image"]>>image;

	if(matf.isOpened() && (!image.data || image.type() != CV_32FC1))
	{
		Log(Log::WARN)<<fileName<<" dose not contain a valid image";
		matf.release();
		return cv::Mat();
	}
	else if(!image.data)
	{
		Log(Log::WARN)<<"could not open "<<fileName<< "ignoreing";
		matf.release();
		return cv::Mat();
	}
	return image;
}

std::vector<cv::Mat> loadImages(char** fileNames)
{ 
	std::vector<cv::Mat> images;
	for(size_t i = 0; fileNames[i]; ++i)
	{
		cv::Mat tmpImage;
		const std::string str(fileNames[i]);
		if(str.find(".mat") !=  std::string::npos)
			openImageYml(fileNames[i]);
		else	
		 tmpImage = openImageImg(fileNames[i]);
		if(tmpImage.data)
			images.push_back(tmpImage);
		else 
			Log(Log::WARN)<<"can not read image "<<i<<" from "<<fileNames[i]<<'\n';
	}
	return images;
}

int perfromOperation(int operation, char** fileNames, const Config& config)
{
	std::vector<cv::Mat> inImages;
	if(operation == CREATE_MAP || operation == APPLY_MAP || operation == APPLY_CURVE)
	{
		inImages = loadImages(fileNames);
		
		if(inImages.empty()) 
		{
			Log(Log::ERROR)<<"Input images must be provided";
			return -1;
		}
	}
	
	if(operation == CREATE_CHARUCO)
	{
		std::string fileName = config.output.empty() ? "out.png" : config.output;
		createCharucoBoard(config.size*14, fileName);
		Log(Log::INFO)<<"Exported charuco map of size "<<config.size*14<<" to "<<fileName;
		return 0;
	}
	else if(operation == CREATE_MAP)
	{
		cv::Mat mask;
		if(config.verbose)
		{
			cv::imshow( "Viewer", inImages[0] );
			cv::waitKey(0);
		}
		if(!config.bg.empty())
		{
			cv::Mat bg = cv::imread(config.bg);
		
			if(bg.data)
			{
				createMask(inImages[0], mask, bg);
				if(config.verbose)
				{
					cv::Mat masked;
					inImages[0].copyTo(masked, mask);
					cv::imshow( "Viewer", masked );
					cv::waitKey(0);
				}
			}
			else Log(Log::WARN)<<"can not read background image from "<<config.bg;
		}
		std::vector<DetectedPoint> points;
		
		if(config.harris)
			points = harrisDetectPoints(inImages[0], mask);
		else
			points = detectCharucoPoints(inImages[0], config.verbose);
		
		Log(Log::INFO)<<"Found "<<points.size()<<" points";
		if(points.size() < 8) 
		{
			Log(Log::ERROR)<<"Error creating map, insufficant points detected";
			return -1;
		}
		
		RemapMap map;
		map.outputCellSize = 50;
		createRemapMap(inImages[0], map, points, config.verbose);
		saveRemapMap(map, fileNames[0]);
	}
	else if(operation == APPLY_MAP)
	{
		std::vector<RemapedImage> remapedImages;
		
		for(size_t i = 0; i<inImages.size(); ++i)
		{
			Log(Log::INFO)<<"Processing image "<<i;
			RemapMap map = loadRemapMap(std::string(fileNames[i])+".mat");
			map.outputCellSize = config.size;
			
			if(!map.xMat.data)
				return -1;
			
			RemapedImage norm;
			if(!config.norm.empty())
			{
				cv::Mat tmp = cv::imread(config.norm);
				if(!tmp.data)
				{
					Log(Log::WARN)<<"could not open normalize file " <<config.norm;
				}
				norm = applyRemap(tmp, map);
				if(config.verbose)
				{
					cv::imshow("Viewer", norm.image);
					cv::waitKey(0);
				}
			}
			
			Log(Log::INFO)<<"Remaping image";
			RemapedImage remaped = applyRemap(inImages[i], map);
			
			Log(Log::INFO)<<"Normalizeing image";
			if(norm.image.data) normalize(remaped.image, norm.image);
			
			if(config.verbose)
			{
				cv::imshow( "Viewer", remaped.image );
				cv::waitKey(0);
			}
			
			remapedImages.push_back(remaped);
		}
			
		cv::Mat out;
		
		Log(Log::INFO)<<"Stiching images";
		if(config.simpleStich)
			out = simpleStich(remapedImages);
		else 
			out = stich(remapedImages);
		
		if(config.verbose)
		{
			cv::imshow( "Viewer", out );
			cv::waitKey(0);
		}
		
		cv::imwrite(!config.output.empty() ? config.output : "out.png", out);
	}
	else if(operation == APPLY_CURVE)
	{
		if(config.curve.empty())
		{
			Log(Log::INFO)<<"a curve must be supplied";
			return -1;
		}
		
		cv::FileStorage fs(config.curve, cv::FileStorage::READ);
		cv::Mat curve;
		fs["curve"]>>curve;
		if(!curve.data || curve.type() != CV_32FC1 || curve.rows != 2 || curve.cols < 3)
		{
			Log(Log::INFO)<<"invalid curve";
			return -1;
		}
		
		cvtColor(inImages[0], inImages[0], cv::COLOR_BGR2GRAY);
		inImages[0].convertTo(inImages[0], CV_32F);
		
		applyCurve(inImages[0], curve);
	}
	else if(operation == CREATE_CURVE)
	{
		cv::Mat curve = cv::Mat::zeros(2, 10, CV_32FC1);
		float* keys   = curve.ptr<float>(0);
		float* values = curve.ptr<float>(1);
		std::cout<<"Please type 10 coordinate pairs\n";
		for(int i = 0; i < curve.cols; ++i)
		{
			std::cout<<i<<"> ";
			double key;
			double value;
			
			std::cin>>key;
			if(std::cin.fail())
			{
				std::cin.clear();
				--i;
				continue;
			}
			std::cin>>value;
			if(std::cin.fail())
			{
				std::cin.clear();
				--i;
				continue;
			}
			
			
			keys[i] = key;
			values[i] = value;
		}
		
		for(int i = 0; i < curve.cols; ++i)
			std::cout<<keys[i]<<' '<<values[i]<<'\n';
		
		cv::FileStorage fs(!config.output.empty() ? config.output : "curve.mat", cv::FileStorage::WRITE);
		fs<<"curve"<<curve;
		fs.release();
		
		std::cout<<"Curve saved to "<<(!config.output.empty() ? config.output : "curve.mat")<<'\n';
	}
	else if(operation == EXIT)
		return -1;
	return 0;
}

int main(int argc, char* argv[])
{
	cv::ocl::setUseOpenCL(false);
	
	Config config;
    argp_parse(&argp, argc, argv, 0, 0, &config);
	
	Log::level = config.quiet ? Log::WARN : config.verbose ? Log::DEBUG : Log::INFO;
	
	Log(Log::INFO)<<"UVOS Optical lubricant thikness mapper "<<argp_program_version;

	if(config.verbose) 
	{
		cv::namedWindow( "Viewer", cv::WINDOW_NORMAL );
		cv::resizeWindow("Viewer", 960, 500);
	}
	
	int operation = selectOperation(config.commandsFiles);
	
	int ret = 0;
	
	if(!config.interactive)
	{
		if(operation < 0)
		{
			Log(Log::ERROR)<<"An operation must be selected";
			Log(Log::INFO)<<"Possible operations: apply create curve mkcurve mkboard";
			return -1;
		}
		ret = perfromOperation(operation, config.commandsFiles+1, config);
	}
	else
	{
		while(true)
		{
			std::cout<<"> ";
			char cmdline[1024];
			char** tokens = new char*[256];
			char *token;
			char *r = cmdline;
			
			for(size_t i = 0; i < 256/sizeof(char*); ++i) 
				tokens[i] = nullptr;
			
			std::cin.getline(cmdline, sizeof(cmdline), '\n');
			
			for(size_t i = 0; (token = strtok_r(r, " ", &r)) && i < 256; ++i)
			{
				tokens[i] = new char[strlen(token)+1];
				strcpy(tokens[i], token);
			}
			
			int operation = selectOperation(tokens);
			if(operation < 0)
			{
				Log(Log::ERROR)<<"A operation must be selected";
				continue;
			}
			
			if(perfromOperation(operation, tokens+1, config) < 0)
				break;
			
			for(size_t i = 0; i < 256/sizeof(char*) && tokens[i]; ++i) 
				delete[] tokens[i];
			delete[] tokens;
		}
	}
	
	if(config.verbose)
	{
		cv::destroyWindow("Viewer");  
		cv::waitKey(0);
	}
	
	return ret;
}

