var Anticaptcha = function(clientKey) {
	return new function(clientKey) {
		this.params = {
			host: 'api.anti-captcha.com',
			port: 80,
			clientKey: clientKey,

			// reCAPTCHA 2
			websiteUrl: null,
			websiteKey: null,
			websiteSToken: null,
			proxyType: 'http',
			proxyAddress: null,
			proxyPort: null,
			proxyLogin: null,
			proxyPassword: null,
			userAgent: '',
			cookies: '',

			// reCAPTCHA 3
			minScore: '',
			pageAction: '',

			// FunCaptcha
			websitePublicKey: null,

			// GeeTest
			websiteChallenge: null,
			geetestApiServerSubdomain: null,

			// image
			phrase: null,
			case: null,
			numeric: null,
			math: null,
			minLength: null,
			maxLength: null,

			// CustomCaptcha
			imageUrl: null,
			assignment: null,
			forms: null,

			softId: null,
			languagePool: null
		};

		var connectionTimeout = 20,
			firstAttemptWaitingInterval = 5,
			normalWaitingInterval = 2;

		this.getBalance = function (cb) {
			var postData = {
				clientKey: this.params.clientKey
			};

			this.jsonPostRequest('getBalance', postData, function (err, jsonResult) {
				if (err) {
					return cb(err, null, jsonResult);
				}

				cb(null, jsonResult.balance, jsonResult);
			});
		};

		this.createTask = function (cb, type, taskData) {
			type = typeof type == 'undefined' ? 'NoCaptchaTask' : type;
			var taskPostData = this.getPostData(type);
			taskPostData.type = type;

			// Merge incoming and already fetched taskData, incoming data has priority
			if (typeof taskData == 'object') {
				for (var i in taskData) {
					taskPostData[i] = taskData[i];
				}
			}

			var postData = {
				clientKey: this.params.clientKey,
				task: taskPostData,
				softId: this.params.softId !== null ? this.params.softId : 0
			};

			if (this.params.languagePool !== null) {
				postData.languagePool = this.params.languagePool;
			}

			this.jsonPostRequest('createTask', postData, function (err, jsonResult) {
				if (err) {
					return cb(err, null, jsonResult);
				}

				// Task created
				var taskId = jsonResult.taskId;

				cb(null, taskId, jsonResult);
			});
		};

		this.createTaskProxyless = function (cb) {
			this.createTask(cb, 'NoCaptchaTaskProxyless');
		};

		this.createRecaptchaV3TaskProxyless = function (cb) {
			this.createTask(cb, 'RecaptchaV3TaskProxyless');
		};

		this.createFunCaptchaTask = function(cb) {
			this.createTask(cb, 'FunCaptchaTask');
		};

		this.createFunCaptchaTaskProxyless = function(cb) {
			this.createTask(cb, 'FunCaptchaTaskProxyless');
		};

		this.createGeeTestTask = function(cb) {
			this.createTask(cb, 'GeeTestTask');
		};

		this.createGeeTestTaskProxyless = function(cb) {
			this.createTask(cb, 'GeeTestTaskProxyless');
		};

		this.createImageToTextTask = function (taskData, cb) {
			this.createTask(cb, 'ImageToTextTask', taskData);
		};

		this.createCustomCaptchaTask = function (cb) {
			this.createTask(cb, 'CustomCaptchaTask');
		};

		this.getTaskRawResult = function(jsonResult) {
			if (typeof jsonResult.solution.gRecaptchaResponse != 'undefined') {
				return jsonResult.solution.gRecaptchaResponse;
			} else if (typeof jsonResult.solution.token != 'undefined') {
				return jsonResult.solution.token;
			} else if (typeof jsonResult.solution.answers != 'undefined') {
				return jsonResult.solution.answers;
			} else if (typeof jsonResult.solution.text !== 'undefined') {
				return jsonResult.solution.text;
			} else {
				return jsonResult.solution;
			}
		}

		this.getTaskSolution = function (taskId, cb, currentAttempt, tickCb) {
			currentAttempt = currentAttempt || 0;

			var postData = {
				clientKey: this.params.clientKey,
				taskId: taskId
			};

			var waitingInterval;
			if (currentAttempt == 0) {
				waitingInterval = firstAttemptWaitingInterval;
			} else {
				waitingInterval = normalWaitingInterval;
			}

			console.log('Waiting %s seconds', waitingInterval);

			var that = this;

			setTimeout(function() {
				that.jsonPostRequest('getTaskResult', postData, function (err, jsonResult) {
					if (err) {
						return cb(err, null, jsonResult);
					}

					if (jsonResult.status == 'processing') {
						// Every call I'm ticki-ing
						if (tickCb) {
							tickCb();
						}
						return that.getTaskSolution(taskId, cb, currentAttempt + 1, tickCb);
					} else if (jsonResult.status == 'ready') {
						return cb(
							null,
							that.getTaskRawResult(jsonResult),
							jsonResult
						);
					}
				});
			}, waitingInterval * 1000);
		};

		this.getPostData = function(type) {
			switch (type) {
				case 'CustomCaptchaTask':
					return {
						imageUrl:       this.params.imageUrl,
						assignment:     this.params.assignment,
						forms:          this.params.forms
					};
				case 'ImageToTextTask':
					return {
						phrase:         this.params.phrase,
						case:           this.params.case,
						numeric:        this.params.numeric,
						math:           this.params.math,
						minLength:      this.params.minLength,
						maxLength:      this.params.maxLength
					};
					break;
				case 'NoCaptchaTaskProxyless':
					return {
						websiteURL:     this.params.websiteUrl,
						websiteKey:     this.params.websiteKey,
						websiteSToken:  this.params.websiteSToken
					};
					break;
				case 'RecaptchaV3TaskProxyless':
					return {
						websiteURL:     this.params.websiteUrl,
						websiteKey:     this.params.websiteKey,
						minScore:       this.params.minScore,
						pageAction:     this.params.pageAction,
					};
					break;
				case 'FunCaptchaTask':
					return {
						websiteURL:         this.params.websiteUrl,
						websitePublicKey:   this.params.websitePublicKey,
						proxyType:          this.params.proxyType,
						proxyAddress:       this.params.proxyAddress,
						proxyPort:          this.params.proxyPort,
						proxyLogin:         this.params.proxyLogin,
						proxyPassword:      this.params.proxyPassword,
						userAgent:          this.params.userAgent,
						cookies:            this.params.cookies
					};
					break;
				case 'FunCaptchaTaskProxyless':
					return {
						websiteURL:         this.params.websiteUrl,
						websitePublicKey:   this.params.websitePublicKey,
					}
				case 'GeeTestTask':
					return {
						websiteURL:                 this.params.websiteUrl,
						gt:                         this.params.websiteKey,
						challenge:                  this.params.websiteChallenge,
						geetestApiServerSubdomain:  this.params.geetestApiServerSubdomain,

						proxyType:                  this.params.proxyType,
						proxyAddress:               this.params.proxyAddress,
						proxyPort:                  this.params.proxyPort,
						proxyLogin:                 this.params.proxyLogin,
						proxyPassword:              this.params.proxyPassword,
						userAgent:                  this.params.userAgent,
						cookies:                    this.params.cookies
					};
					break;
				case 'GeeTestTaskProxyless':
					return {
						websiteURL:                 this.params.websiteUrl,
						gt:                         this.params.websiteKey,
						challenge:                  this.params.websiteChallenge,
						geetestApiServerSubdomain:  this.params.geetestApiServerSubdomain,
					}
				default: // NoCaptchaTask
					return {
						websiteURL:     this.params.websiteUrl,
						websiteKey:     this.params.websiteKey,
						websiteSToken:  this.params.websiteSToken,
						proxyType:      this.params.proxyType,
						proxyAddress:   this.params.proxyAddress,
						proxyPort:      this.params.proxyPort,
						proxyLogin:     this.params.proxyLogin,
						proxyPassword:  this.params.proxyPassword,
						userAgent:      this.params.userAgent,
						cookies:        this.params.cookies
					};
			}


		};

		this.jsonPostRequest = function(methodName, postData, cb) {
			if (typeof process === 'object' && typeof require === 'function') { // NodeJS
				var http = require('http');

				// http request options
				var options = {
					hostname: this.params.host,
					port: this.params.port,
					path: '/' + methodName,
					method: 'POST',
					headers: {
						'accept-encoding':  'gzip,deflate',
						'content-type':     'application/json; charset=utf-8',
						'accept':           'application/json',
						'content-length':   Buffer.byteLength(JSON.stringify(postData))
					}
				};

				// console.log(options);
				// console.log(JSON.stringify(postData));

				var req = http.request(options, function(response) { // on response
					var str = '';

					// another chunk of data has been recieved, so append it to `str`
					response.on('data', function (chunk) {
						str += chunk;
					});

					// the whole response has been recieved, so we just print it out here
					response.on('end', function () {
						// console.log(str);

						try {
							var jsonResult = JSON.parse(str);
						} catch (err) {
							return cb(err);
						}

						if (jsonResult.errorId) {
							return cb(new Error(jsonResult.errorDescription, jsonResult.errorCode), jsonResult);
						}

						return cb(null, jsonResult);
					});
				});

				// send post data
				req.write(JSON.stringify(postData));
				req.end();

				// timeout in milliseconds
				req.setTimeout(connectionTimeout * 1000);
				req.on('timeout', function() {
					console.log('timeout');
					req.abort();
				});

				// After timeout connection throws Error, so we have to handle it
				req.on('error', function(err) {
					console.log('error');
					return cb(err);
				});

				return req;
			} else if ((typeof window !== 'undefined' || typeof chrome === 'object') && typeof $ == 'function') { // in browser or chrome extension with jQuery
				$.ajax(
					  (window.location.protocol == 'https:' ? 'https:' : 'http:') + '//'
					+ this.params.host
					+ (window.location.protocol != 'https:' ? ':' + this.params.port : '')
					+ '/' + methodName,
					{
						method: 'POST',
						data: JSON.stringify(postData),
						dataType: 'json',
						success: function (jsonResult) {
							if (jsonResult && jsonResult.errorId) {
								return cb(new Error(jsonResult.errorDescription, jsonResult.errorCode), jsonResult);
							}

							cb(false, jsonResult);
						},
						error: function (jqXHR, textStatus, errorThrown) {
							cb(new Error(textStatus != 'error' ? textStatus : 'Unknown error, watch console')); // should be errorThrown
						}
					}
				);
			} else {
				console.error('Application should be run either in NodeJs environment or has jQuery to be included');
			}
		};

		this.setClientKey = function (value) {
			this.params.clientKey = value;
		};

		//proxy access parameters
		this.setWebsiteURL = function (value) {
			this.params.websiteUrl = value;
		};

		this.setWebsiteKey = function (value) {
			this.params.websiteKey = value;
		};

		this.setMinScore = function (value) {
			this.params.minScore = value;
		};

		this.setPageAction = function (value) {
			this.params.pageAction = value;
		};

		this.setWebsiteSToken = function (value) {
			this.params.websiteSToken = value;
		};

		this.setWebsitePublicKey = function (value) {
			this.params.websitePublicKey = value;
		};

		this.setWebsiteChallenge = function (value) {
			this.params.websiteChallenge = value;
		};

		this.setGeetestApiServerSubdomain = function (value) {
			this.params.geetestApiServerSubdomain = value;
		};

		this.setProxyType = function (value) {
			this.params.proxyType = value;
		};

		this.setProxyAddress = function (value) {
			this.params.proxyAddress = value;
		};

		this.setProxyPort = function (value) {
			this.params.proxyPort = value;
		};

		this.setProxyLogin = function (value) {
			this.params.proxyLogin = value;
		};

		this.setProxyPassword = function (value) {
			this.params.proxyPassword = value;
		};

		this.setUserAgent = function (value) {
			this.params.userAgent = value;
		};

		this.setCookies = function (value) {
			this.params.cookies = value;
		};

		// image
		this.setPhrase = function (value) {
			this.params.phrase = value;
		};

		this.setCase = function (value) {
			this.params.case = value;
		};

		this.setNumeric = function (value) {
			this.params.numeric = value;
		};

		this.setMath = function (value) {
			this.params.math = value;
		};

		this.setMinLength = function (value) {
			this.params.minLength = value;
		};

		this.setMaxLength = function (value) {
			this.params.maxLength = value;
		};

		this.setImageUrl = function (value) {
			this.params.imageUrl = value;
		};

		this.setAssignment = function (value) {
			this.params.assignment = value;
		};

		this.setForms = function (value) {
			this.params.forms = value;
		};

		this.setSoftId = function (value) {
			this.params.softId = value;
		};

		this.setLanguagePool = function (value) {
			this.params.languagePool = value;
		};

		this.setHost = function (value) {
			this.params.host = value;
		};

		this.setPort = function (value) {
			this.params.port = value;
		};

	}(clientKey);
};

if (typeof process === 'object' && typeof require === 'function') { // NodeJS
	module.exports = Anticaptcha;
}