/*
PolyGun - tool for extracting translatable strings from source code

Copyright (c) 2023 mrkubax10 <mrkubax10@onet.pl>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#include <iostream>
#include <cstring>
#include <filesystem>
#include <vector>
#include <fstream>
#include <algorithm>
#include <sstream>

#include "common/xml/lexer.hpp"
#include "common/xml/parser.hpp"
#include "common/xml/xml_node.hpp"

void print_help(const char* executable_name) {
	std::cout<<"Tool that extracts translatable strings from source code"<<std::endl;
	std::cout<<executable_name<<" <input folders> [arguments...]"<<std::endl;
	std::cout<<"--help                             Show this message"<<std::endl;
	std::cout<<"--language, --lang <languages>     Extract from files with certain language (possible values: cpp, xml)"<<std::endl;
	std::cout<<"--output, -o <output file>         Set output file to <output file>"<<std::endl;
}

void list_folder(std::vector<std::string>& output, const std::string& input, const std::vector<std::string>& extensions) {
	for(const std::filesystem::directory_entry& entry : std::filesystem::directory_iterator(input)) {
		if(entry.is_directory())
			list_folder(output, entry.path().string(), extensions);
		else {
			bool found = false;
			for(const std::string& extension : extensions) {
				if(extension==entry.path().extension().string()) {
					found = true;
					break;
				}
			}
			if(!found)
				continue;
			output.push_back(entry.path().string());
		}
	}
}

void split_string(std::vector<std::string>& output, const std::string& str, char ch) {
	std::stringstream ss(str);
	std::string element;
	while(getline(ss, element, ch))
		output.push_back(element);
}

struct StringData {
	std::string m_data;
	std::string m_translation;
	std::string m_file;
	size_t m_line;
};
void cpp_extract_strings(const std::string& input_filename, std::vector<StringData>& output) {
	std::ifstream input(input_filename);
	if(!input.good()) {
		std::cout<<" failed to open file";
		return;
	}

	size_t line = 1;
	while(!input.eof()) {
		char ch = input.get();
		if(isspace(ch)) {
			if(ch=='\n')
				line++;
			while(!input.eof() && isspace(ch = input.get())) {
				if(ch=='\n')
					line++;
			}
			if(input.eof())
				break;
		}
		switch(ch) {
		case '/':
			ch = input.get();
			if(ch=='/') {
				while(!input.eof() && input.get()!='\n');
				line++;
			}
			else if(ch=='*') {
				char ch2;
				while(!input.eof() && (ch=input.get())!='*' && (ch2=input.get())!='/') {
					if(ch=='\n')
						line++;
					if(ch2=='\n')
						line++;
				}
			}
			break;
		case '#':
			while(!input.eof() && input.get()!='\n');
			line++;
			break;
		case '"':{
			bool escaped = false;
			while(!input.eof()) {
				ch = input.get();
				if(!escaped && ch=='"')
					break;
				if(escaped)
					escaped = false;
				if(ch=='\\')
					escaped = true;
			}
			break;
		}
		case '_':
			if(input.eof())
				break;
			while(isspace((ch = input.get())));
			if(ch=='(') {
				while(isspace((ch = input.get())));
				if(ch=='"') {
					std::string temp;
					while((ch = input.get())!='"')
						temp+=ch;
					while(isspace((ch = input.get())));
					std::string filename_copy = input_filename;
					std::replace(filename_copy.begin(), filename_copy.end(), '\\', '/');
					output.push_back(StringData{temp, "", filename_copy, line+1});
				}
				else {
					std::cout<<"Expected '(' at "<<input_filename<<":"<<line<<std::endl;
					input.close();
				}
			}
			break;
		}
	}

	input.close();
}
void xml_extract_strings(const std::string& input_filename, std::vector<StringData>& output) {
	std::ifstream input(input_filename);
	if(!input.good()) {
		std::cout<<" failed to open file";
		return;
	}

	std::vector<polygun::xml::XMLToken> tokens;
	if(!polygun::xml::tokenize(input, tokens)) {
		std::cout<<" XML tokenization failed";
		return;
	}

	size_t offset = 0;
	while(offset<tokens.size()) {
		if(tokens[offset].m_type==polygun::xml::XMLToken::XMLTokenType::XML_TOKEN_TYPE_IDENTIFIER) {
			if(tokens[offset].m_data!="tr_text") {
				offset++;
				continue;
			}
			if(++offset>tokens.size())
				break;
			if(tokens[offset].m_type==polygun::xml::XMLToken::XMLTokenType::XML_TOKEN_TYPE_EQUALS) {
				if(++offset>tokens.size())
					break;
				if(tokens[offset].m_type==polygun::xml::XMLToken::XMLTokenType::XML_TOKEN_TYPE_STRING)
					output.push_back(StringData{tokens[offset].m_data, "", input_filename, tokens[offset].m_line});
			}
		}
		offset++;
	}

	input.close();
}

int main(int argc, char** argv) {
	if(argc<2) {
		std::cout<<"Usage:"<<std::endl;
		print_help(argv[0]);
		return 1;
	}

	// Arguments
	std::vector<std::string> arg_languages = {"cpp"};
	std::string arg_output;
	std::vector<std::string> arg_inputs;

	// Parse command line arguments
	for(int i = 1; i<argc; i++) {
		if(strcmp(argv[i], "--help")==0) {
			print_help(argv[0]);
			return 0;
		}
		else if(strcmp(argv[i], "--language")==0 || strcmp(argv[i], "--lang")==0) {
			if(++i>=argc) {
				std::cout<<argv[i-1]<<" requires <language> argument"<<std::endl;
				return 1;
			}
			std::vector<std::string> temp;
			split_string(temp, argv[i], ',');
			for(const std::string& lang : temp) {
				std::vector<std::string>::iterator it = std::find(arg_languages.begin(), arg_languages.end(), lang);
				if(it==arg_languages.end())
					arg_languages.push_back(lang);
			}
		}
		else if(strcmp(argv[i], "--output")==0 || strcmp(argv[i], "-o")==0) {
			if(++i>=argc) {
				std::cout<<argv[i-1]<<" requires <output file> argument"<<std::endl;
				return 1;
			}
			arg_output = argv[i];
		}
		else if(strlen(argv[i])>=1 && argv[i][0]=='-'){
			std::cout<<"Invalid argument "<<argv[i]<<std::endl;
			return 1;
		}
		else
			arg_inputs.push_back(argv[i]);
	}
	if(arg_inputs.empty()) {
		std::cout<<"Missing input folder"<<std::endl;
		return 1;
	}
	if(arg_output.empty())
		arg_output = "a.xml";

	std::vector<std::string> extensions;
	for(const std::string& lang : arg_languages) {
		if(lang=="cpp") {
			extensions.push_back(".cpp");
			extensions.push_back(".hpp");
			extensions.push_back(".h");
			extensions.push_back(".cc");
			extensions.push_back(".hh");
			extensions.push_back(".cxx");
			extensions.push_back(".hxx");
			extensions.push_back(".c++");
			extensions.push_back(".h++");
		}
		else if(lang=="xml")
			extensions.push_back(".xml");
		else {
			std::cout<<"Invalid language "<<lang<<std::endl;
			return 1;
		}
	}
	std::vector<std::string> files;
	for(const std::string& input : arg_inputs)
		list_folder(files, input, extensions);

	std::vector<StringData> strings;
	for(const std::string& entry : files) {
		std::cout<<"Processing "<<entry<<"...";
		std::string extension = std::filesystem::path(entry).extension().string();
		if(extension==".cpp")
			cpp_extract_strings(entry, strings);
		else if(extension==".xml")
			xml_extract_strings(entry, strings);
		std::cout<<std::endl;
	}
	std::cout<<"Found "<<strings.size()<<" string(s)"<<std::endl;

	std::ifstream temp(arg_output);
	if(temp.good()) {
		polygun::xml::XMLNode* root;
		if(polygun::xml::parse(temp, root)) {
			if(root->get_name()=="locale") {
				for(polygun::xml::XMLNode* node = root->next_node("tr"); node; node = root->next_node("tr")) {
					polygun::xml::XMLNode* msgid = node->next_node("msgid");
					if(!msgid)
						continue;
					polygun::xml::XMLNode* msgstr = node->next_node("msgstr");
					if(!msgstr)
						continue;
					for(StringData& str : strings) {
						if(str.m_data==msgid->get_text()) {
							str.m_translation = msgstr->get_text();
							break;
						}
					}
				}
			}
			delete root;
		}
		temp.close();
	}
	
	std::ofstream output(arg_output);
	if(!output.good()) {
		std::cout<<"Failed to open '"<<arg_output<<"' for writing"<<std::endl;
		return 1;
	}

	output<<"<!--"<<std::endl<<"Polygun locale"<<std::endl<<"Language:"<<std::endl<<"Last translator:"<<std::endl<<"-->"<<std::endl<<std::endl;
	output<<"<locale>"<<std::endl;
	for(const StringData& str : strings) {
		output<<"\t<!-- "<<str.m_file<<":"<<str.m_line<<" -->"<<std::endl;
		output<<"\t<tr>"<<std::endl;
		output<<"\t\t<msgid>"<<str.m_data<<"</msgid>"<<std::endl;
		output<<"\t\t<msgstr>"<<str.m_translation<<"</msgstr>"<<std::endl;
		output<<"\t</tr>"<<std::endl;
	}
	output<<"</locale>"<<std::endl;

	output.close();
	return 0;
}
