define(['jquery',], function ($) {
	var STATE_INITIAL = 'input';
	var STATE_LOADING = 'loading';
	var STATE_PREVIEW_LOADING = 'preview-loading';
	var STATE_PREVIEW = 'preview';
	var STATE_INPUT_PREVIEW = 'input-preview';
	var STATE_ERROR = 'error';

	var EVENT_SOURCE_TYPE_MESSAGE = 'message';
	var EVENT_SOURCE_TYPE_FINISH = 'finish';
	var EVENT_SOURCE_TYPE_TERMINATED = 'terminated';

	var TRIGGER_EVENT_ON_OPEN = 'onOpen';

	var AiTextGenerator = function () {
		var $this = this;

		this.Initialise = function (config, plugins) {
			this.$modal = $('#ai-generate-content-modal');
			this.$clear_button = this.$modal.find('.js-clear-input-textarea');
			this.$copy_button = this.$modal.find('.js-copy-content');
			this.$submit_button = this.$modal.find('.js-submit-content');
			this.$text_prompt_area = this.$modal.find('.js-textarea-prompt');
			this.$preview_area = this.$modal.find('.js-preview-area');
			this.$preview = this.$modal.find('.js-preview');
			this.$text_preview_area = this.$modal.find('.js-textarea-preview');

			this.$target = null;
			this.module = config.module;
			this.meta_data = config.meta_data ? config.meta_data : null;
			this.stream = config.stream;
			this.language = config.language;
			this.feedback_touched = false;
			this.break_scrolling = false;
			this.plugins = [];

			if (config.target_id) {
				var target = $('#' + config.target_id);

				if (target.length) {
					this.$target = target;
				}
			}

			if (plugins) {
				for (var i = 0; i < plugins.length; i++) {
					var plugin = plugins[i];

					plugin.install(this);

					this.plugins.push(plugin);
				}
			}

			this.state = STATE_INITIAL;
			// Speed in this case is the interval, so we need to make the value smaller
			this.speed = (100 / config.speed)
			this.text_position = 0;
			this.buffer = '';

			this.event_source = null;

			this.ShowState(STATE_INITIAL);

			this.Listeners();
		}

		this.ShowLoading = function (toggle) {
			if (toggle) {
				if (!this.$submit_button.find('.spinner-grow').length) {
					this.$submit_button.append('<span class="spinner-grow spinner-grow-sm ml-1" role="status" aria-hidden="true"></span>');
				}

				this.$submit_button.prop('disabled', true);
				this.$text_prompt_area.prop('disabled', true);
				this.$clear_button.prop('disabled', true);
				this.ShowValidationError('');
			} else {
				this.$submit_button.find('.spinner-grow').remove();
				this.$submit_button.prop('disabled', false);
				this.$text_prompt_area.prop('disabled', false);
				this.$clear_button.prop('disabled', false);
			}
		};

		this.TogglePreviewArea = function (toggle) {
			// Each time the preview are is toggled,
			// clear out the contents
			this.$text_preview_area.empty();

			if (toggle) {
				this.$preview_area.show();
				this.AppendToPreviewArea();
			} else {
				this.break_scrolling = false;
				this.$preview_area.hide();
			}
		};

		this.AppendToPreviewArea = function () {
			var retry_count = 0;
			var is_tag = false;
			var entity_in_progress = false;
			var entity_buffer = '';

			var TypeWriter = function () {
				var text = $this.buffer.slice(0, $this.text_position++);

				var char = text.slice(-1);

				if (char === '<') {
					is_tag = true;
				} else if (char === '>') {
					is_tag = false;
				}

				// If there's a tag, immediately go onto the next iteration
				if (is_tag) {
					setTimeout(TypeWriter, $this.speed);
					return;
				}

				// Handle HTML entities
				if (char === '&') {
					entity_buffer = char
					entity_in_progress = true;
					TypeWriter();
					return;
				}

				if (entity_in_progress) {
					entity_buffer += char;
					if (char === ';') {
						// We've reached the end of the entity, so write it to our text
						entity_in_progress = false;
						text = text.slice(0, -entity_buffer.length) + entity_buffer;
					} else {
						// Keep skipping
						TypeWriter();
						return;
					}
				}

				$this.$text_preview_area.html(text);

				if ($this.break_scrolling === false) {
					// Keep the preview scrolled to the bottom so the end
					// user can see the response being generated
					$this.$preview.scrollTop($this.$preview[0].scrollHeight);
				}

				if ($this.text_position < $this.buffer.length)
					setTimeout(TypeWriter, $this.speed);
				else if ($this.state === STATE_PREVIEW_LOADING) {
					// If our state is still loading and we ran out of text to output
					// This could mean we are potentially still waiting for the buffer to be buffered
					// So keep trying to see if we got a remaining buffer before updating the state
					if (retry_count++ < 5) {
						setTimeout(TypeWriter, 500);
					} else {
						$this.ShowState(STATE_PREVIEW);
					}
				}
			};

			if (this.stream)
				TypeWriter();
			else
				this.$text_preview_area.html(this.buffer);
		};

		this.ResizePromptTextArea = function () {
			this.$text_prompt_area.height(this.$text_prompt_area[0].scrollHeight);
		}

		this.CanCopyToClipBoard = function () {
			return typeof navigator.clipboard !== 'undefined' && window.isSecureContext;
		};

		this.ResetPointers = function () {
			this.buffer = '';
			this.text_position = 0;

			// Close the connection if it is still open
			if (this.event_source && this.event_source.readyState !== EventSource.CLOSED)
				this.event_source.close();

			this.event_source = null;
		};

		this.HasTarget = function () {
			return this.$target !== null && this.$target.length > 0;
		}

		this.UpdateFooter = function () {
			switch (this.state) {
				case STATE_INITIAL:
					{
						this.$copy_button.hide();
						this.$submit_button.show().text(lmsg('common.ai.generative_content.modal.generate'));
					}
					break;
				case STATE_LOADING:
					{
						this.$copy_button.hide();
					}
					break;
				case STATE_PREVIEW:
					{
						if (this.CanCopyToClipBoard())
							this.$copy_button.show();

						if (this.HasTarget())
							this.$submit_button.show().text(lmsg('common.ai.generative_content.modal.insert'));
						else
							this.$submit_button.prop('disabled', true);
					}
					break;
				case STATE_INPUT_PREVIEW:
					{
						if (this.CanCopyToClipBoard())
							this.$copy_button.show();

						if (this.HasTarget())
							this.$submit_button.show().text(lmsg('common.ai.generative_content.modal.regenerate'));
						else
							this.$submit_button.prop('disabled', false);
					}
					break;
			}
		}

		this.ShowState = function (stateType, data) {
			switch (stateType) {
				case STATE_INITIAL:
					{
						this.$text_prompt_area.val('');

						this.ResetValidation();
						this.ResetPointers();
						this.ShowLoading(false);
						this.TogglePreviewArea(false);
					}
					break;
				case STATE_LOADING:
					{
						this.ResetPointers();
						this.ShowLoading(true);
						this.TogglePreviewArea(false);
					}
					break;
				case STATE_PREVIEW_LOADING:
					{
						this.buffer = data

						if (this.state != stateType)
						{
							this.ShowLoading(true);
							this.TogglePreviewArea(true);
						}
					}
					break;
				case STATE_PREVIEW:
					{
						this.ShowLoading(false);

						if (data && !this.stream)
						{
							this.buffer = data
							this.TogglePreviewArea(true);
						}
					}
					break;
				case STATE_ERROR:
					{
						this.ShowLoading(false);
						this.TogglePreviewArea(true);

						var $error = $('<span></span>');
						$error.text(lmsg('common.ai.invalid_prompt'));
						this.$text_preview_area.html($error);
					}
					break;
				case STATE_INPUT_PREVIEW:
					break;
			}

			this.state = stateType;

			this.UpdateFooter();
		};

		this.GetValidationErrorMessage = function () {
			if (this.$text_prompt_area.val().length < 10) {
				return lmsg('common.ai.prompt.validation.more_than', 10);
			}

			if (this.$text_prompt_area.val().length > 1000) {
				return lmsg('common.ai.prompt.validation.less_than', 1000);
			}

			return '';
		};

		this.ShowValidationError = function () {
			// Don't show validation on the first attempt
			if (!this.feedback_touched) {
				return;
			}

			var $feed_back = this.$modal.find('.invalid-feedback');
			var error_message = this.GetValidationErrorMessage();

			$feed_back.text(error_message);

			if (error_message) {
				this.$text_prompt_area.addClass('is-invalid');
			} else {
				this.$text_prompt_area.removeClass('is-invalid');
			}
		};

		this.IsValidationValid = function () {
			return this.GetValidationErrorMessage() == '';
		};

		this.ResetValidation = function() {
			this.feedback_touched = false;
			this.$text_prompt_area.removeClass('is-invalid');
		};

		this.TriggerEvent = function(eventName, eventData) {
			for (var i = 0; i < this.plugins.length; i++) {
				var plugin = this.plugins[i];

				if (typeof plugin[eventName] === 'function') {
					plugin[eventName](eventData);
				}
			}
		};

		this.SetPrompt = function (text) {
			this.$text_prompt_area.val(text);
		};

		this.Listeners = function () {
			this.$submit_button.on('click', function (event) {
				event.preventDefault();

				$this.feedback_touched = true;

				if ($this.state === STATE_PREVIEW && $this.HasTarget()) {
					// We need to see if the target is a CKEDITOR
					if (typeof CKEDITOR !== "undefined") {
						if (CKEDITOR.instances[$this.$target.attr('id')]) {
							CKEDITOR.instances[$this.$target.attr('id')].setData($this.buffer, function () {
								this.updateElement();
							});
						}
					} else if ($this.$target.is('input:text')) {
						$this.$target.val($this.buffer);
					}

					$this.$modal.modal('hide');
				} else {
					if (!$this.IsValidationValid()) {
						$this.ShowValidationError();
						return;
					}

					$this.ShowState(STATE_LOADING);

					if ($this.stream)
					{
						$this.event_source = new EventSource('/api/ai/generate-text-stream?prompt=' + $this.$text_prompt_area.val() + '&module=' + $this.module + '&language=' + $this.language);

						$this.event_source.onmessage = function (event) {
							var data = JSON.parse(event.data);

							switch (data.status) {
								case EVENT_SOURCE_TYPE_MESSAGE:
									{
										$this.ShowState(STATE_PREVIEW_LOADING, data.text);
									}
									break;
								case EVENT_SOURCE_TYPE_TERMINATED:
									{
										// If the state is preview loading, this means the state
										// is still writing out to the screen so show a meow error instead
										if ($this.state != STATE_PREVIEW_LOADING)
											$this.ShowState(STATE_ERROR);
										else
											cla.showMessage(lmsg('common.ai.response_terminated'), '', true)
									}
									break;
								case EVENT_SOURCE_TYPE_FINISH:
									{
										$this.event_source.close();
									}
									break;
								default:
									break;
							}
						}

						$this.event_source.onerror = function () {
							// If the state is preview loading this means we are still writing
							// to the screen therefore it will show what we've got in our buffer
							// so no need to do anything here
							if ($this.state === STATE_PREVIEW_LOADING) {
								return;
							}

							// We've unexpectedly encountered an error
							// If there's no text written to the preview area
							// then show an error, else show what we have available
							if (!$.trim($this.$text_preview_area.html()).length)
								$this.ShowState(STATE_ERROR);
							else
							{
								// We don't want to show the preview state if the text position is more than the buffer length
								// because this would mean we're still displaying the text to the preview area
								if ($this.text_position >= $this.buffer.length) {
									$this.ShowState(STATE_PREVIEW);
								}
							}

							$this.event_source.close();
						}
					}
					else
					{
						$.post('/api/ai/generate-text', {
							prompt: $this.$text_prompt_area.val(),
							module: $this.module,
							language: $this.language
						}, function(data) {
							$this.ShowState(STATE_PREVIEW, data.text);
						}).fail(function () {
							$this.ShowState(STATE_ERROR);
						});
					}
				}
			});

			this.$text_prompt_area.on('keyup', function () {
				if ($this.state === STATE_PREVIEW)
					$this.ShowState(STATE_INPUT_PREVIEW);

				$this.ShowValidationError();
			});

			this.$clear_button.on('click', function () {
				if ($this.state === STATE_PREVIEW)
					$this.ShowState(STATE_INPUT_PREVIEW);

				$this.$text_prompt_area.val('');
			});

			this.$copy_button.on('click', function () {
				if ($this.CanCopyToClipBoard()) {

					// Convert HTML to plain text, then afterwards remove the element from the DOM
					var tempElement = document.createElement('div');
					tempElement.innerHTML = $this.buffer;
					navigator.clipboard.writeText(tempElement.textContent);
					tempElement.remove();

					$.meow({
						title: lmsg('common.ai.generative_content.modal.clipboard.title'),
						message: lmsg('common.ai.generative_content.modal.clipboard.description'),
						className: 'inner-success'
					});
				}
			});

			// This is to break the auto scrolling when generating the content
			// The user may want to read from the beginning as we continue to generate the content
			this.$preview.on('mousewheel', function () {
				if ($this.$preview[0].scrollHeight > $this.$preview[0].clientHeight)
					$this.break_scrolling = true;
				else
					$this.break_scrolling = false;
			});

			this.$modal.on('shown.bs.modal', function () {
				$this.ShowState(STATE_INITIAL);

				$this.TriggerEvent(TRIGGER_EVENT_ON_OPEN);
			});

			this.$modal.on('hidden.bs.modal', function () {
				$this.ShowState(STATE_INITIAL);
			});
		};

		return this;
	};

	return new AiTextGenerator();
});
