/**
 * TalkToWebsite client JS scripts
 *
 * @link   https://talktowebsite.com/
 * @file   This file contains client-side JS scripts designed to run the TalkToWebsite AI plugin.
 * @author Vitalii A. Popov
 * @since  1.0.0
 */

/* jshint esversion: 6 */
/* jshint strict: true */
/* jshint bitwise: false */
/* globals vappv_ttw_public_js, jQuery, crypto */

( function ( $ ) {

	"use strict";

	window.wp = window.wp || {};

	// TTW public scripts
	let talkToWebsite = {

		// Is chat content loaded first time
		isChatLoadedFirstTime: false,

		// Is the webchat contains more messages in backward scrolling direction (from newest to oldest messages)
		areMoreMessagesInBackwardDirection: true,

		// Is chat text area should be active on chat open (first time chat)
		isChatTextAreaShouldBeActiveOnOpen: false,

		// Admin chat messages loading mutex (true | false)
		chatMessagesLoadingMutex: false,

		// Mutex to prevent messages multi-sending
		sendChatMessageMutex: false,

		// Admin dialogue messages cache
		chatMessagesCache: [],

		// Chat modal window
		$chatWindow: null,

		// Content area quick link
		$chatMessagesArea: null,

		// Website original appearance (mobile devices adjustments)
		websiteDefaultAppearanceParts: {
			viewport: null,
			viewportIsSetInitially: false,
			htmlOverflowProperty: null,
			bodyOverflowProperty: null
		},

		// Settings cache (ony part of them)
		pluginSettings: {
			robotName: 'AI',
			robotMessagePostScriptum: null
		},

		// Emergency storage if both cookies and localStorage are not working
		emergencyDataStorage: {},

		// ------------------------------------------------------------------

		/**
		 * Create new GUID string
		 * @returns {string}
		 */
		generateUUIDv4: function () {
			return ( [1e7] + -1e3 + -4e3 + -8e3 + -1e11 ).replace( /[018]/g, c =>
				( c ^ crypto.getRandomValues( new Uint8Array( 1 ) )[0] & 15 >> c / 4 ).toString( 16 )
			);
		},

		/**
		 * Check is Email valid
		 * @param {string} email
		 * @returns {boolean}
		 */
		checkIsEmail: function( email ) {
			let regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
			return regex.test( email );
		},

		/**
		 * Process specified time part / prepend leading '0' to number if necessary (from x to [?0]x)
		 * @param {number} timePart
		 * @returns {string|*}
		 */
		processTimePart: function( timePart ) {
			if ( timePart ) {
				if ( timePart.toString().length === 1 ) {
					return `0${timePart}`;
				} else {
					return timePart.toString();
				}
			}
			else if ( timePart === 0 ) {
				return '00';
			}
			else {
				return '';
			}
		},

		/**
		 * Format date in YYYY-MM-DD style
		 * @param {Date} date
		 * @returns {string|*}
		 */
		formatDateYYYYMMDD: function( date ) {
			if ( ! date ) {
				return date;
			}
			else if ( Object.prototype.toString.call( date ) !== '[object Date]' ) {
				return date.toString();
			}
			else {
				return date.getFullYear() + '-' + talkToWebsite.processTimePart( date.getMonth() + 1 ) + '-' + talkToWebsite.processTimePart( date.getDate() );
			}
		},

		/**
		 * Format date in YYYY-MM-DD HH:mm:ss style
		 * @param {Date} date Date object
		 * @returns {string|*}
		 */
		formatDateYYYYMMDD_HHMMSS: function( date ) {
			if ( ! date ) {
				return date;
			}
			else if ( Object.prototype.toString.call( date ) !== '[object Date]' ) {
				return date.toString();
			}
			else {
				return talkToWebsite.formatDateYYYYMMDD( date ) + ' ' + talkToWebsite.processTimePart( date.getHours() ) + ':' + talkToWebsite.processTimePart( date.getMinutes() ) + ':' + talkToWebsite.processTimePart( date.getSeconds() );
			}
		},

		/**
		 * Format date in YYYY-MM-DD HH:mm:ss style
		 * @param {Date} date
		 * @returns {string|*}
		 */
		formatDateYYYYMMDD_comma_HHMM: function( date ) {
			if ( ! date ) {
				return date;
			}
			else if ( Object.prototype.toString.call( date ) !== '[object Date]' ) {
				return date.toString();
			}
			else {
				return talkToWebsite.formatDateYYYYMMDD( date ) + ', ' + talkToWebsite.processTimePart( date.getHours() ) + ':' + talkToWebsite.processTimePart( date.getMinutes() );
			}
		},

		/**
		 * Prepend item to the array
		 * @param {Object} value Value to prepend
		 * @param {Array} targetArray Target array to prepend an item
		 * @returns {string|*}
		 */
		arrayPrepend: function( value, targetArray ) {
			let newArray = targetArray.slice();
			newArray.unshift( value );
			return newArray;
		},

		/**
		 * Check is mobile device used
		 * @returns {boolean}
		 */
		isMobileDeviceUsed: function () {
			if ( /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4)) ) {
				return true;
			}
			else {
				return false;
			}
		},

		/**
		 * Linkify plain text: replace all plain URLs to <a href> tags
		 * @param {string} inputText
		 * @returns {string}
		 */
		linkifyPlainText: function( inputText ) {
			let replacedText, replacePattern1, replacePattern2, replacePattern3;

			// URLs starting with http://, https://, or ftp://
			replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
			replacedText = inputText.replace( replacePattern1, '<a href="$1" target="_blank" data-role="ttw-linkifyed-url">$1</a>' );

			// URLs starting with "www." (without // before it, or it'd re-link the ones done above).
			replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
			replacedText = replacedText.replace( replacePattern2, '$1<a href="http://$2" target="_blank" data-role="ttw-linkifyed-url">$2</a>' );

			// Change email addresses to mailto:: links.
			replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
			replacedText = replacedText.replace( replacePattern3, '<a href="mailto:$1" data-role="ttw-linkifyed-url">$1</a>' );

			return replacedText;
		},

		/**
		 * Add markup to the text (chat **text** to <b>text</b>, ~~text~~ to <i>text</i> etc.)
		 * @param {string} inputText
		 * @returns {string}
		 */
		markupPlainText: function( inputText ) {
			if ( inputText ) {
				let inputTextWithMarkup = inputText.replace( /\*\*(.*?)\*\*/g, "<b>$1</b>" );
				inputTextWithMarkup = inputTextWithMarkup.replace( /__(.*?)__/g, "<u>$1</u>" );
				inputTextWithMarkup = inputTextWithMarkup.replace( /~~(.*?)~~/g, "<i>$1</i>" );
				inputTextWithMarkup = inputTextWithMarkup.replace( /<<(.*?)>>/g, "<a href='$1'>Link</a>" );
				return inputTextWithMarkup;
			}
			else {
				return '';
			}
		},

		/**
		 * Prepare message text to drawing
		 * @param {string} inputText
		 * @returns {string}
		 */
		prepareMessageText: function( inputText ) {
			if ( inputText ) {
				// Linkify text
				let inputTextPrepared = talkToWebsite.linkifyPlainText( inputText );
				inputTextPrepared = inputTextPrepared.replaceAll( '</a>.;', '</a> ;' );
				inputTextPrepared = inputTextPrepared.replaceAll( '</a>.', '</a> ;' );

				// Replace newlines to <br /><br />
				inputTextPrepared = inputTextPrepared.replace( /(?:\r\n|\r|\n)/g, '<br /><br />' );

				// Replace long newline sequences
				while ( inputTextPrepared.indexOf( '<br /><br /><br />' ) !== -1 ) {
					inputTextPrepared = inputTextPrepared.replaceAll( '<br /><br /><br />', '<br /><br />' );
				}

				// Replace ** to <b>
				inputTextPrepared = talkToWebsite.markupPlainText( inputTextPrepared );

				return inputTextPrepared;
			}
			else {
				return '';
			}
		},

		/**
		 * Set document cookie to specified value
		 * @param {string} name Cookie name
		 * @param {string} value Cookie value
		 * @param {number} days Cookie lifetime, days count
		 */
		setCookie: function( name, value, days ) {
			let expires = '';
			if ( days ) {
				let date = new Date();
				date.setTime( date.getTime() + ( days * 24 * 60 * 60 * 1000 ) );
				expires = '; expires=' + date.toUTCString();
			}
			document.cookie = name + '=' + ( value || '' )  + expires + '; path=/';
		},

		/**
		 * Get document cookie by its name
		 * @param {string} name Cookie name
		 * @returns {string|null}
		 */
		getCookie: function( name ) {
			let nameEQ = name + '=';
			let ca = document.cookie.split( ';' );
			for ( let i = 0; i < ca.length; i++ ) {
				let c = ca[ i ];
				while ( c.charAt( 0 ) === ' ' ) {
					c = c.substring( 1, c.length );
				}
				if ( c.indexOf( nameEQ ) === 0 ) {
					return c.substring( nameEQ.length, c.length );
				}
			}
			return null;
		},

		/**
		 * Erase specified cookie
		 * @param name Cookie name
		 */
		eraseCookie: function( name ) {
			document.cookie = name + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
		},

		/* ------------------------------------------------------------------ */

		/**
		 * Initialize TalkToWebsite plugin (public part)
		 */
		initializePlugin: function () {

			if ( $( '#vappv-ttw-plugin-disabled' ).length > 0 ) {
				// Plugin is disabled by some reason. Don't do anything.
				return;
			}

			// Add activity class to the 'body' section
			$( 'body' ).addClass( 'ttw-plugin-active' );

			// Get data from settings (inputs displayed on the page)
			if ( $( '#ttw-settings-robot-name' ).length > 0 ) {
				talkToWebsite.pluginSettings.robotName = $( '#ttw-settings-robot-name' ).val().trim();
			}
			if ( $( '#ttw-settings-robot-messages-post-scriptum' ).length > 0 ) {
				talkToWebsite.pluginSettings.robotMessagePostScriptum = talkToWebsite.prepareMessageText( $( '#ttw-settings-robot-messages-post-scriptum' ).val().trim() );
			}

			// Create quick link to the dialogue area
			talkToWebsite.$chatMessagesArea = $( '#ttw-dialogue-content-area' );
			talkToWebsite.$chatWindow = $( '#vappv-ttw-chat-dialogue' );
			talkToWebsite.isChatTextAreaShouldBeActiveOnOpen = true;

			// Get current user data
			let currentUserGUIDFromTheLocalStorage = talkToWebsite.localStorageGet( 'vappv-ttw-userguid' );
			if ( typeof currentUserGUIDFromTheLocalStorage === 'undefined' || currentUserGUIDFromTheLocalStorage === null || currentUserGUIDFromTheLocalStorage === '' ) {
				// Chat started first time
				// Show 'Introduce yourself' content area
				talkToWebsite.$chatWindow.find( '#ttw-introduce-yourself-area' ).show();
				talkToWebsite.$chatWindow.find( '#ttw-dialogue-area' ).hide();
				talkToWebsite.$chatWindow.find( '#ttw-ai-is-sleeping-now-area' ).hide();
				talkToWebsite.isChatTextAreaShouldBeActiveOnOpen = false;
			}

			if ( $( '#ttw_daily_limit_exhausted' ).val() === 'yes' ) {
				talkToWebsite.$chatWindow.find( '#ttw-introduce-yourself-area' ).hide();
				talkToWebsite.$chatWindow.find( '#ttw-dialogue-area' ).hide();
				talkToWebsite.$chatWindow.find( '#ttw-ai-is-sleeping-now-area' ).show();
				talkToWebsite.isChatTextAreaShouldBeActiveOnOpen = false;
			}

			// Subscribe floating bubble behavior
			$( '#ttw-chat-floating-bubble' ).off( 'click' ).on( 'click', function( e ) {
				if ( talkToWebsite.$chatWindow.is( ':visible' ) === false ) {
					talkToWebsite.openChat();
				}
				else {
					talkToWebsite.closeChat();
				}
			} );

			// Subscribe 'Introduce yourself' button
			$( '#ttw-start-chat-with-ai-button' ).off( 'click' ).on( 'click', function( e ) {

				// Resetting the background of fields in the 'Please introduce yourself' form
				$( '#ttw-introduce-yourself-errors-area' ).empty().hide();
				$( '#ttw-introduce-yourself-name, #ttw-introduce-yourself-email' ).css( 'background', '' );

				let userName = $( '#ttw-introduce-yourself-name' ).val().trim();
				let userEmail = $( '#ttw-introduce-yourself-email' ).val().trim();
				let errorsList = [];

				if ( userName === null || userName === '' ) {
					$( '#ttw-introduce-yourself-name' ).css( 'background', '#ffdbdb' );
					errorsList.push( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-enteryourname' ) );
				}

				if ( userEmail !== null && userEmail !== '' ) {
					if ( talkToWebsite.checkIsEmail( userEmail ) === false ) {
						$( '#ttw-introduce-yourself-email' ).css( 'background', '#ffdbdb' );
						errorsList.push( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-enteryouremail' ) );
					}
				}

				if ( errorsList.length > 0 ) {
					for ( let q = 0; q < errorsList.length; q++ ) {
						errorsList[ q ] = `(${( q + 1 )}) ${errorsList[ q ]}`;
					}
					$( '#ttw-introduce-yourself-errors-area' ).html( errorsList.join( ';<br />' ) ).show();
					return;
				}

				talkToWebsite.localStorageSet( 'vappv-ttw-userguid', talkToWebsite.generateUUIDv4() );
				talkToWebsite.localStorageSet( 'vappv-ttw-useremail', userEmail );
				talkToWebsite.localStorageSet( 'vappv-ttw-username', userName );

				$( '#ttw-introduce-yourself-area' ).hide();
				$( '#ttw-dialogue-area' ).show();
			} );

			// Subscribe 'Back to website' button
			$( '#ttw-cancel-chat-with-ai-button-from-introduction-window' ).off( 'click' ).on( 'click', function( e ) {
				talkToWebsite.closeChat();
			} );

			// Subscribe click on the 'Send chat message' button
			$( '#ttw-dialogue-send-message-button' ).off( 'click' ).on( 'click', function () {
				talkToWebsite.tryToSendChatMessageFromUser();
			} );

			// Subscribe 'Enter / Return' keypress on input
			$( '#ttw-dialogue-message-text' ).off( 'keyup' ).on( 'keyup', function( e ) {
				if ( e.key === 'Enter' ) {
					talkToWebsite.tryToSendChatMessageFromUser();
				}
			} );

			// Handle scroll event into 'Dialogue' tab
			talkToWebsite.$chatMessagesArea.off( 'scroll' ).on( 'scroll', function( event ) {
				talkToWebsite.handleDialogueScrolled();
			} );

			// Handle click on 'View archive messages' button
			$( '#ttw-view-my-conversation-history-with-sleeping-robot' ).off( 'click' ).on( 'click', function( e ) {
				$( '#ttw-ai-is-sleeping-now-area' ).hide();
				$( '#ttw-dialogue-area' ).show();
				$( '#ttw-dialogue-send-message-button-outer' ).remove();
				$( '.ttw-dialogue-interaction-area' ).addClass( 'ttw-robot-sleeping-ta-background' );
				$( '.ttw-dialogue-window-triangle-cloud-arrow' ).addClass('ttw-robot-sleeping-ta-background-triangle' );
				let robotIsRestingPlaceholderText = $( '#ttw-js-messages' ).attr( 'data-ttwmessages-robotisresting_placeholder' );
				$( '#ttw-dialogue-message-text' ).prop( 'disabled', true ).addClass( 'ttw-robot-is-sleeping-textarea' ).attr( 'placeholder', robotIsRestingPlaceholderText );
			} );

			// Handle click on 'Know more about plugin' button
			$( '#ttw-click-here-to-know-more-about-plugin' ).off( 'click' ).on( 'click', function( e ) {
				$( '#ttw-dialogue-area' ).hide();
				$( '#ttw-about-area' ).show();
			} );

			// Handle click on 'Back to chat' button (about plugin section)
			$( '#ttw-ai-about-back-to-chat-button' ).off( 'click' ).on( 'click', function( e ) {
				$( '#ttw-dialogue-area' ).show();
				$( '#ttw-about-area' ).hide();
			} );

			// Handle 'Collapse chat' from mobile version (header)
			$( '#ttw-button-close-chat-from-mobile' ).off( 'click' ).on( 'click', function( e ) {
				talkToWebsite.closeChat();
			} );

			// Subscribe scroll to the textarea on mobile devices on focus
			$( '#ttw-dialogue-message-text' ).off( 'focus' ).on( 'focus', function( e ) {
				if ( talkToWebsite.isMobileDeviceUsed() ) {
					$( '#ttw-dialogue-message-text' ).get( 0 ).scrollIntoView();
					setTimeout( function () {
						$( '#ttw-dialogue-message-text' ).get( 0 ).scrollIntoView();
					}, 200 );
				}
			} );

			// Promo popup: click on 'Lets chat' button
			$( '#ttw-promo-popup-lets-chat-button' ).off( 'click' ).on( 'click', function( e ) {
				talkToWebsite.closePromoPopupOverlay();
				talkToWebsite.openChat();
			} );

			// Promo popup: clicn on 'Close dialogue' button
			$( '#ttw-promo-popup-close-button' ).off( 'click' ).on( 'click', function( e ) {
				talkToWebsite.closePromoPopupOverlay();
			} );

			// Load user messages
			if ( talkToWebsite.isChatLoadedFirstTime === false ) {
				talkToWebsite.getAndDrawAllUserMessages( null, null );
				talkToWebsite.isChatLoadedFirstTime = true;
			}

			// Handle click on promo popup overlay
			$( '#ttw-chat-website-background-overlay-mobile' ).off( 'click' ).on( 'click', function( e ) {
				talkToWebsite.closePromoPopupOverlay();
			} );

			// Handle "Escape" keypress
			$( document ).on('keyup', function( e ) {
				if ( e.key === 'Escape' ) {
					talkToWebsite.closeChat();
					talkToWebsite.closePromoPopupOverlay();
				}
			} );
		},

		/**
		 * Handle promo popup displaying (show promo popup for new users, do nothing for already known users).
		 * Works only if according settings set to value not equal to -1
		 */
		handlePromoPopupDisplaying: function () {
			// Handle promo popup displaying
			if ( $( '#ttw-settings-show-promo-popup-after-n-seconds' ).length > 0 ) {
				// Get displaying delay (after page loaded)
				let aiPromoPopupDisplayingDelay = $( '#ttw-settings-show-promo-popup-after-n-seconds' ).val();
				aiPromoPopupDisplayingDelay = Number( aiPromoPopupDisplayingDelay );

				// Set timeout
				if ( aiPromoPopupDisplayingDelay > -1 ) {
					// Get reshow time frequency in minutes
					let promoPopupShowingFrequencyMinutes = Number( $( '#ttw-settings-show-promo-popup-time-frequency-n-minutes' ).val() );
					let promoPopupShownAndClosedLastTime = talkToWebsite.localStorageGet( 'vappv-ttw-promopopup-displayed-and-closed-last-time' );
					if ( promoPopupShownAndClosedLastTime !== null ) {
						promoPopupShownAndClosedLastTime = Number( promoPopupShownAndClosedLastTime );
					}
					let canShowPopupByFrequency = true;

					if ( promoPopupShowingFrequencyMinutes === -1 && promoPopupShownAndClosedLastTime !== null ) {
						// Popup is already shown, and we cannot show it again (promoPopupShowingFrequencyMinutes = -1)
						canShowPopupByFrequency = false;
					}
					else if ( promoPopupShowingFrequencyMinutes !== -1 && promoPopupShownAndClosedLastTime !== null ) {
						// Popup is already shown, but we can show it again after [promoPopupShowingFrequencyMinutes] minutes
						let currentDate = new Date();
						let lastShownDate = new Date();
						lastShownDate.setTime( promoPopupShownAndClosedLastTime );

						// Get time difference in seconds / minutes
						let datesDiffSeconds = ( currentDate.getTime() - lastShownDate.getTime() ) / 1000;
						let datesDiffMinutes = ( Math.round( datesDiffSeconds / 60 ) );

						if ( datesDiffMinutes < promoPopupShowingFrequencyMinutes ) {
							canShowPopupByFrequency = false;
						}
					}

					// Set popup displaying timeout
					if ( canShowPopupByFrequency === true ) {
						setTimeout( function () {
							// Check is main chat already displayed
							if ( $( '#vappv-ttw-chat-dialogue' ).is( ':visible' ) === false ) {
								// All fine, show the promo popup
								talkToWebsite.openPromoPopupOverlay();
							}
						}, ( aiPromoPopupDisplayingDelay * 1000 ) );
					}
				}
			}
		},

		/**
		 * Open promo popup overlay
		 */
		openPromoPopupOverlay: function () {
			$( '#vappv-ttw-chat-promo-popup, #ttw-chat-website-background-overlay-mobile' ).show();
		},

		/**
		 * Check is local storage accessible for this device
		 **/
		isLocalStorageAccessible: function () {
			try {
				if ( window.localStorage ) {
					return true;
				}
				else {
					return false;
				}
			}
			catch( e ) {
				return false;
			}
		},

		/**
		 * Check is cookies accessible for this device
		 */
		isCookiesAccessible: function () {
			try {
				// Save and read test cookie
				talkToWebsite.setCookie( 'ttw-checkpoint', 'yes', 180 );
				let ttwCheckpointValue = talkToWebsite.getCookie( 'ttw-checkpoint' );
				if ( ttwCheckpointValue === 'yes' ) {
					return true;
				}
				else {
					return false;
				}
			}
			catch( e ) {
				return false;
			}
		},

		/**
		 * Safety set item into local storage
		 */
		localStorageSet: function( tname, tvalue ) {

			// Check is local storage / cookies accessible
			let isLocalStorageAccessible = talkToWebsite.isLocalStorageAccessible();
			let isCookiesAccessible = talkToWebsite.isCookiesAccessible();

			// Try save data into local storage
			if ( isLocalStorageAccessible === true ) {
				localStorage.setItem( tname, tvalue );
				if ( localStorage.getItem( tvalue ) === tvalue ) {
					return;
				}
			}

			// Try save data into cookie
			if ( isCookiesAccessible === false ) {
				talkToWebsite.setCookie( tname, tvalue, 180 );
				if ( talkToWebsite.getCookie( tname ).toString() === tvalue.toString() )
					return;
			}

			// Save data into emergency data storage
			talkToWebsite.emergencyDataStorage[ tname ] = tvalue;
		},

		/**
		 * Safety get item from local storage
		 */
		localStorageGet: function( tname ) {

			// Check is local storage / cookies accessible
			let isLocalStorageAccessible = talkToWebsite.isLocalStorageAccessible();
			let isCookiesAccessible = talkToWebsite.isCookiesAccessible();

			// Get value from local storage
			if ( isLocalStorageAccessible === true ) {
				return localStorage.getItem( tname );
			}

			// Get value from cookies
			if ( isCookiesAccessible === true ) {
				return talkToWebsite.getCookie( tname );
			}

			// Get value from current JS storage
			if ( talkToWebsite.emergencyDataStorage[ tname ] ) {
				return talkToWebsite.emergencyDataStorage[ tname ];
			}

			return null;
		},

		/**
		 * Close promo popup overlay
		 */
		closePromoPopupOverlay: function () {
			$( '#vappv-ttw-chat-promo-popup, #ttw-chat-website-background-overlay-mobile' ).hide();
			let currentDate = new Date();
			talkToWebsite.localStorageSet( 'vappv-ttw-promopopup-displayed-and-closed-last-time', currentDate.getTime() );
		},

		/**
		 * Open chat window
		 */
		openChat: function () {

			// Check for opened promo popup and close it if necessary
			if ( $( '#vappv-ttw-chat-promo-popup' ).is( ':visible' ) === true ) {
				talkToWebsite.closePromoPopupOverlay();
			}

			// Open chat
			talkToWebsite.$chatWindow.show();

			// Adjust chat on mobile devices
			if ( talkToWebsite.isMobileDeviceUsed() ) {
				// Swap viewport
				if ( $( 'meta[name="viewport"]' ).length > 0 ) {
					talkToWebsite.websiteDefaultAppearanceParts.viewport = $( 'meta[name="viewport"]' ).attr( 'content' );
					$( 'meta[name="viewport"]' ).attr( 'content', 'width=device-width, initial-scale=1, maximum-scale=1' );
					talkToWebsite.websiteDefaultAppearanceParts.viewportIsSetInitially = true;
				}
				else {
					talkToWebsite.websiteDefaultAppearanceParts.viewportIsSetInitially = false;
					$( 'head' ).append( '<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />' );
				}

				// Hide page scrolling
				$( 'html, body' ).css( {
					overflow: 'hidden',
					height: '100%'
				} );
			}

			// Check textarea focusing (only for native devices -- never opens on mobile)
			if ( talkToWebsite.isChatTextAreaShouldBeActiveOnOpen === true && talkToWebsite.isMobileDeviceUsed() === false ) {
				talkToWebsite.$chatMessagesArea.scrollTop( talkToWebsite.$chatMessagesArea.prop( 'scrollHeight' ) );
				$( '#ttw-dialogue-message-text' ).select().focus();
			}
			else {
				talkToWebsite.$chatMessagesArea.scrollTop( talkToWebsite.$chatMessagesArea.prop( 'scrollHeight' ) );
			}
		},

		/**
		 * Close chat window
		 */
		closeChat: function () {
			// Close chat
			talkToWebsite.$chatWindow.hide();

			// Restore mobile devices
			if ( talkToWebsite.isMobileDeviceUsed() ) {
				// Restore viewport
				if ( talkToWebsite.websiteDefaultAppearanceParts.viewportIsSetInitially === true ) {
					$( 'meta[name="viewport"]' ).attr( 'content', talkToWebsite.websiteDefaultAppearanceParts.viewport );
				}
				else {
					$( 'meta[name="viewport"]' ).remove();
				}

				// Restore scrolling
				$( 'html, body' ).css( {
					overflow: '',
					height: ''
				} );
			}
		},

		/**
		 * Mark URL into message as followed
		 * @param {number} messageId Message numeric identifier
		 * @param {string} messageGUID Message text identifier (GUID)
		 * @param {string} followedURL Followed URL text
		 */
		markURLIntoMessageAsFollowed: function( messageId, messageGUID, followedURL ) {

			// Send message to the backend
			$.post(
				vappv_ttw_public_js.ajaxurl,
				{
					action: 'vappv_ttw_set_urls_followed_into_message',
					message_id: messageId,
					message_guid: messageGUID,
					followed_url: followedURL,
					nonce: vappv_ttw_public_js.nonce
				},
				function( response ) {

					let processedFollowedURLData = null;

					// Decode response
					try {
						processedFollowedURLData = JSON.parse( response );
					}
					catch( error ) {
						// Something went wrong (don't display any information to the public user)
						// talkToWebsite.displayError( 'Error when unpacking JSON from the server. Call of the "ttw_set_urls_followed_into_message" function failed.' );
						return;
					}

					//Handle errors
					if ( processedFollowedURLData.AJAXCallResults === 'ERROR' ) {
						// Managed error
						// Something went wrong (don't display any information to the public user)
						// talkToWebsite.displayError( `Cannot mark URL as followed. Error code = ${processedFollowedURLData.TTWErrorCode}; Error details = ${processedFollowedURLData.ErrorDetails}` );
					}
					else if ( processedFollowedURLData.AJAXCallResults === 'SUCCESS' ) {
						// Managed success
						// ... do nothing ...
					}
					else {
						// Something went wrong (don't display any information to the public user)
						// talkToWebsite.displayError( 'Cannot mark URL as followed (Unexpected server response). Please contact support.' );
					}
				}
			).fail( function( xhr, textStatus, errorThrown ) {
				// Something went wrong (don't display any information to the public user)
				// talkToWebsite.displayError( 'Cannot mark URL as followed (AJAX call failed). Please contact support.' );
			} );

		},

		/**
		 * Get admin dialogue messages list
		 * @param {string} dataDirection dataDirection = { 'backward' || 'forward' }
		 * @param {number} lastKnownID ID of the last known message
		 */
		getAndDrawAllUserMessages: function( dataDirection, lastKnownID ) {

			// Check for user GUID
			let currentUserGUID = talkToWebsite.localStorageGet( 'vappv-ttw-userguid' );
			if ( typeof currentUserGUID === 'undefined' || currentUserGUID === null || currentUserGUID === '' ) {
				// No messages from the user yet.
				// Render 'Welcome' message
				talkToWebsite.areMoreMessagesInBackwardDirection = false;
				talkToWebsite.renderWelcomeToTheChatMessage();
				talkToWebsite.handlePromoPopupDisplaying();
				return;
			}

			// Set mutex and load data
			if ( talkToWebsite.chatMessagesLoadingMutex === true ) {
				return;
			}
			talkToWebsite.chatMessagesLoadingMutex = true;

			// Prepare params
			let limit = 25;
			if ( typeof dataDirection === 'undefined' || dataDirection === null ) {
				dataDirection = 'backward';
			}
			if ( typeof lastKnownID === 'undefined' || lastKnownID === null ) {
				lastKnownID = null;
			}

			// Draw waiting loader
			if ( lastKnownID === null || dataDirection === 'forward' ) {
				talkToWebsite.renderLoadingMessages();
			}
			else if ( dataDirection === 'backward' ) {
				talkToWebsite.renderLoadingMessages( 'before' );
			}

			$.post(
				vappv_ttw_public_js.ajaxurl,
				{
					action: 'vappv_ttw_list_user_messages',
					user_guid: currentUserGUID,
					data_direction: dataDirection,
					last_known_id: lastKnownID,
					data_limit: limit,
					nonce: vappv_ttw_public_js.nonce
				},
				function( response ) {

					let userMessagesResponse = null;
					talkToWebsite.chatMessagesLoadingMutex = false;

					// Decode response
					try {
						userMessagesResponse = JSON.parse( response );
					}
					catch( error ) {
						talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-get-user-messages-history-jsonexception' ) );
						return;
					}

					// Remove loader
					$( '#ttw-loading-user-messages-text-block' ).remove();

					// Handle errors
					if ( userMessagesResponse.AJAXCallResults === 'ERROR' ) {
						if ( userMessagesResponse.TTWErrorCode === 'USER_NO_THREADS_YET' ) {
							// No messages from the user yet.
							// Render 'Welcome' message
							talkToWebsite.renderWelcomeToTheChatMessage();
							return;
						}
						else {
							talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-get-user-messages-history-common ') );
						}
					}
					else if ( userMessagesResponse.AJAXCallResults === 'SUCCESS' ) {

						// Managed success: itterate messages and update data cache
						for ( let q = 0; q < userMessagesResponse.Data.Messages.length; q++ ) {

							// Convert time to UTC based
							if ( userMessagesResponse.Data.Messages[q].message_date.indexOf( 'Z' ) === -1 ) {
								userMessagesResponse.Data.Messages[q].message_date += 'Z';
							}

							// Append message to the cache
							if ( dataDirection === 'forward' ) {
								talkToWebsite.chatMessagesCache.push( userMessagesResponse.Data.Messages[q] );
								talkToWebsite.renderChatMessage( userMessagesResponse.Data.Messages[q], 'append', true );
							}
							else if ( dataDirection === 'backward' ) {
								talkToWebsite.chatMessagesCache = talkToWebsite.arrayPrepend( userMessagesResponse.Data.Messages[q], talkToWebsite.chatMessagesCache );
								talkToWebsite.renderChatMessage( userMessagesResponse.Data.Messages[q], 'prepend', false );
							}
						}

						// Check is chat empty at the moment
						if ( talkToWebsite.chatMessagesCache.length > 0 )
						{
							// Check for pending AI responses
							let lastUserMessageFromCache = talkToWebsite.chatMessagesCache[ talkToWebsite.chatMessagesCache.length - 1 ];
							if (
								lastUserMessageFromCache.sender_role === 'user' &&
								( lastUserMessageFromCache.is_responded_by_ai === null || lastUserMessageFromCache.is_responded_by_ai === 0 || lastUserMessageFromCache.is_responded_by_ai === '0' ) &&
								( lastUserMessageFromCache.is_executed_with_error === null || lastUserMessageFromCache.is_executed_with_error === '' )
							) {
								talkToWebsite.tryFetchPendingAIResponses();
							}

							// Try to draw dialogues separators
							talkToWebsite.drawDialoguesSeparators();

							// Scroll page to the bottom if initial loading occured
							if ( lastKnownID === null || dataDirection === 'forward' ) {
								talkToWebsite.$chatMessagesArea.scrollTop( talkToWebsite.$chatMessagesArea.prop( 'scrollHeight' ) );
							}

							// Check movement to the backward direction
							if ( dataDirection === 'backward' ) {
								// Just leave the scroll in current position (revert scroll before new items prepend)
								if ( lastKnownID ) {
									talkToWebsite.$chatMessagesArea.scrollTop( 1 );
									let $last_known_message_id_object = talkToWebsite.$chatMessagesArea.find( `[data-role='ttw-chat-message'][data-id='${lastKnownID}']` );
									$last_known_message_id_object.get( 0 ).scrollIntoView();
								}
								else {
									talkToWebsite.$chatMessagesArea.scrollTop( 1 );
								}

								// Check are more messages during scrolling to the top
								if ( userMessagesResponse.Data.AreMoreMessages === false || userMessagesResponse.Data.AreMoreMessages === 'false' ) {
									talkToWebsite.areMoreMessagesInBackwardDirection = false;
								}
							}
						}
						else if ( userMessagesResponse.Data.Messages.length === 0 ) {
							// User don't have any messages at all
							talkToWebsite.handlePromoPopupDisplaying();
							talkToWebsite.areMoreMessagesInBackwardDirection = false;
						}

						// Check for sleeping state
						if (
							talkToWebsite.chatMessagesCache.length > 0 &&
							$( '#ttw_daily_limit_exhausted' ).val() === 'yes'
						) {
							$( '#ttw-view-my-conversation-history-with-sleeping-robot' ).show();
						}
						else {
							$( '#ttw-view-my-conversation-history-with-sleeping-robot' ).remove();
						}
					}
					else {
						// Something went wrong
						talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-get-user-messages-history-unexpected-response' ) );
					}
				}
			).fail( function( xhr, textStatus, errorThrown ) {
				talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-get-user-messages-history-ajaxexception' ) );
				talkToWebsite.chatMessagesLoadingMutex = false;
			} );
		},

		/**
		 * Lock chat input field and prevent messaging
		 */
		lockChatMessageInputArea: function() {
			talkToWebsite.sendChatMessageMutex = true;
			$( '#ttw-dialogue-message-text' ).val( '' ).prop( 'disabled', true );
			$( '#ttw-dialogue-interaction-area, #ttw-dialogue-window-triangle-cloud-arrow, #ttw-dialogue-send-message-button' ).addClass( 'ttw-disabled' );
			setTimeout( function() {
				$( '#ttw-dialogue-message-text' ).val( '' );
			}, 150);
		},

		/**
		 * Unlock chat input field and allow messaging
		 */
		unlockChatMessageInputArea: function() {
			talkToWebsite.sendChatMessageMutex = false;
			$( '#ttw-dialogue-interaction-area, #ttw-dialogue-window-triangle-cloud-arrow, #ttw-dialogue-send-message-button' ).removeClass( 'ttw-disabled' );
			$( '#ttw-dialogue-message-text' ).prop( 'disabled', false );
			if ( talkToWebsite.isMobileDeviceUsed() === false ) {
				$( '#ttw-dialogue-message-text' ).select().focus();
			}
		},

		/**
		 * Try to send chat message from the user
		 */
		tryToSendChatMessageFromUser: function () {

			// Prevent messages multi-sending
			if ( talkToWebsite.sendChatMessageMutex === true ) {
				return;
			}

			// Send chat message to the admin dialogue
			// Get and validate chat message
			let messageText = $( '#ttw-dialogue-message-text' ).val().trim();

			// Make some preparations to message
			// Remove all unnecessary tags
			messageText = messageText.replaceAll( "`", "'" );
			messageText = messageText.replaceAll( ">", "&gt;" );
			messageText = messageText.replaceAll( "<", "&lt;" );
			messageText = $( '<p id="ttw-message-temp-buffer-notags">' + messageText + '</p>' ).text();
			$( '#ttw-message-temp-buffer-notags' ).remove();

			// Check message for content
			if ( typeof messageText === 'undefined' || messageText === null || messageText.trim().length < 1 ) {
				alert( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-message-is-empty' ) );
				$( '#ttw-dialogue-message-text' ).val( '' ).select().focus();
				talkToWebsite.unlockChatMessageInputArea();
				return;
			}

			// Drop message to the chat window, then draw loader
			let messageGUID = talkToWebsite.generateUUIDv4();
			talkToWebsite.renderChatMessage( {
			   id: -1,
			   message_guid: messageGUID,
			   message_text: messageText,
			   dialogue_id: -1,
			   message_date: new Date().toString(),
			   sender_role: 'user',
			}, 'append', true );

			// Render "Wait for an AI response" block into chat
			talkToWebsite.renderWaitingForAIResponse();

			// Lock messages sending
			talkToWebsite.lockChatMessageInputArea();

			// Send message to the backend
			$.post(
				vappv_ttw_public_js.ajaxurl,
				{
					action: 'vappv_ttw_send_message_as_user',
					user_name: talkToWebsite.localStorageGet( 'vappv-ttw-username' ),
					user_email: talkToWebsite.localStorageGet( 'vappv-ttw-useremail' ),
					user_guid: talkToWebsite.localStorageGet( 'vappv-ttw-userguid' ),
					message_guid: messageGUID,
					message_text: messageText,
					nonce: vappv_ttw_public_js.nonce
				},
				function( response ) {

					let processedUserMessageData = null;

					// Decode response
					try {
						processedUserMessageData = JSON.parse( response );
					}
					catch( error ) {
						talkToWebsite.unlockChatMessageInputArea();
						$( '#ttw-waiting-for-ai-response-text-block' ).remove();
						talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-send-message-jsonexception' ) );
						return;
					}

					// Handle errors
					if ( processedUserMessageData.AJAXCallResults === 'ERROR' ) {
						// Managed error
						talkToWebsite.unlockChatMessageInputArea();
						$( '#ttw-waiting-for-ai-response-text-block' ).remove();
						talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-send-message-common' ) );
					}
					else if ( processedUserMessageData.AJAXCallResults === 'SUCCESS' ) {
						// Managed success
						// Try fetch response from AI
						talkToWebsite.tryFetchPendingAIResponses();

						// Complete message params
						let insertedMessageObject = processedUserMessageData.Data.InsertedMessage;
						let $chatMessageBlock = talkToWebsite.$chatMessagesArea.find( `[data-role="ttw-chat-message"][data-messageguid="${messageGUID}"]` );
						$chatMessageBlock.attr( 'data-dialogue-id', insertedMessageObject.dialogue_id );
						$chatMessageBlock.attr( 'data-id', insertedMessageObject.id );

						// Draw dialogues separators
						talkToWebsite.drawDialoguesSeparators();
						talkToWebsite.$chatMessagesArea.scrollTop( talkToWebsite.$chatMessagesArea.prop( 'scrollHeight' ) );

						// Add message to the cache
						talkToWebsite.chatMessagesCache.push( processedUserMessageData.Data.InsertedMessage );
				   }
				   else {
						// Something went wrong
						talkToWebsite.unlockChatMessageInputArea();
						$( '#ttw-waiting-for-ai-response-text-block' ).remove();
						talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-send-message-unexpected-response' ) );
				   }
				}
			).fail( function( xhr, textStatus, errorThrown ) {
				talkToWebsite.unlockChatMessageInputArea();
				$( '#ttw-waiting-for-ai-response-text-block' ).remove();
				talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-send-message-ajaxexception' ) );
			} );
		},

		/**
		 * Try to fetch pending AI responses
		 */
		tryFetchPendingAIResponses: function () {
			$.post(
				vappv_ttw_public_js.ajaxurl,
				{
					action: 'vappv_ttw_try_get_message_responses_from_ai_as_user',
					user_guid: talkToWebsite.localStorageGet( 'vappv-ttw-userguid' ),
					nonce: vappv_ttw_public_js.nonce
				},
				function( response ) {

					let responseFromAIData = null;

					// Decode response
					try {
						responseFromAIData = JSON.parse( response );
					}
					catch( error ) {
						talkToWebsite.unlockChatMessageInputArea();
						talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-get-ai-response-jsnexception' ) );
						$( '#ttw-waiting-for-ai-response-text-block' ).remove();
						return;
					}

					// Handle errors
					if ( responseFromAIData.AJAXCallResults === 'ERROR' ) {
						talkToWebsite.unlockChatMessageInputArea();
						talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-get-ai-response-common' ) );
						$( '#ttw-waiting-for-ai-response-text-block' ).remove();
						return;
					}
					else if ( responseFromAIData.AJAXCallResults === 'SUCCESS' ) {

						// Handle results
						if ( responseFromAIData.Data.ExtractionResults === 'no_new_messages' ) {
							// There are no pending messages from AI
							talkToWebsite.unlockChatMessageInputArea();
							$( '#ttw-waiting-for-ai-response-text-block' ).remove();
							return;
						}
						if ( responseFromAIData.Data.ExtractionResults === 'last_message_executed_with_error' ) {
							// Last message executed with error
							talkToWebsite.unlockChatMessageInputArea();
							$( '#ttw-waiting-for-ai-response-text-block' ).remove();
							return;
						}
						if ( responseFromAIData.Data.ExtractionResults === 'ai_answer_is_still_pending' ) {
							// Last message executed with error
							setTimeout( function () {
								talkToWebsite.tryFetchPendingAIResponses();
							}, 1500 );
							return;
						}

						//-------------------------------------------------------------------------------------
						// Managed success
						for ( let q = 0; q < responseFromAIData.Data.Messages.length; q++ ) {
							if ( responseFromAIData.Data.Messages[ q ].message_date.indexOf( 'Z' ) === -1 ) {
								responseFromAIData.Data.Messages[ q ].message_date += 'Z';
							}
							talkToWebsite.chatMessagesCache.push( responseFromAIData.Data.Messages[ q ] );
							talkToWebsite.renderChatMessage( responseFromAIData.Data.Messages[ q ], 'append', true );
						}

						talkToWebsite.unlockChatMessageInputArea();
						$( '#ttw-waiting-for-ai-response-text-block' ).remove();
						// -------------------------------------------------------------------------------------
					}
					else {
						// Something went wrong
						talkToWebsite.unlockChatMessageInputArea();
						$( '#ttw-waiting-for-ai-response-text-block' ).remove();
						talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-get-ai-response-unexpected-response' ) );
					}
				}
			).fail( function( xhr, textStatus, errorThrown ) {
				talkToWebsite.unlockChatMessageInputArea();
				$( '#ttw-waiting-for-ai-response-text-block' ).remove();
				talkToWebsite.displayError( $( '#ttw-js-messages' ).attr( 'data-ttwmessages-cannot-get-ai-response-ajaxexception' ) );
			} );
		},

		/**
		 * Handle dialogue block scrolled to the top
		 */
		handleDialogueScrolled: function () {
			let scrollTopPosition = talkToWebsite.$chatMessagesArea.scrollTop();
			if ( scrollTopPosition === 0 ) {
				if ( talkToWebsite.areMoreMessagesInBackwardDirection === true && talkToWebsite.chatMessagesCache.length > 0 ) {
					talkToWebsite.getAndDrawAllUserMessages( 'backward', talkToWebsite.chatMessagesCache[0].id );
				}
			}
		},

		/**
		 * Draw dialogue separators (lines between messages with different dialogue identifiers)
		 */
		drawDialoguesSeparators: function () {
			// Try to find gaps between dialogues (Different GUIDs in two messages)
			let $allRenderedChatMessages = talkToWebsite.$chatMessagesArea.find( '.ttw-chat-message' );
			for ( let f = 0; f < $allRenderedChatMessages.length; f++ ) {
				let $currentMessageBlock = $allRenderedChatMessages.eq( f );
				if ( f + 1 < $allRenderedChatMessages.length ) {
					let $nextMessageBlock = $allRenderedChatMessages.eq( f + 1 );
					if ( $currentMessageBlock.attr( 'data-dialogue-id' ) !== $nextMessageBlock.attr( 'data-dialogue-id' ) && Number( $currentMessageBlock.attr( 'data-dialogue-id' ) ) !== -1 && Number( $nextMessageBlock.attr( 'data-dialogue-id' ) ) !== -1 ) {
						let $dialogues_ids_pair = `${$currentMessageBlock.attr( 'data-dialogue-id' )}-${$nextMessageBlock.attr( 'data-dialogue-id' )}`;
						let $separator_object = talkToWebsite.$chatMessagesArea.find( `.ttw-dialogue-separator[data-dialogue-ids="${$dialogues_ids_pair}"]` );
						if ( ! $separator_object || $separator_object.length === 0 ) {
							$currentMessageBlock.after( `<div class="ttw-dialogue-separator" data-dialogue-ids="${$dialogues_ids_pair}"><div class="ttw-dialogue-separator-content">Dialogue restarted by timeout / limits.<br />The previous dialogue context is reset.</div></div>` );
							f++;
						}
					}
				}
			}
		},

		/**
		 * Render new chat message
		 * @param {string} chatMessage
		 * @param {string} drawType
		 * @param {boolean} autoScroll
		 */
		renderChatMessage: function( chatMessage, drawType, autoScroll ) {

			// Get chat message HTML code
			let messageHTML = talkToWebsite.getChatMessageHTML( chatMessage );

			// Append / Prepend message to the chat
			if ( typeof drawType === 'undefined' || drawType === null || drawType === 'append' ) {
				talkToWebsite.$chatMessagesArea.append( messageHTML );
				if ( autoScroll === true ) {
					talkToWebsite.$chatMessagesArea.scrollTop( talkToWebsite.$chatMessagesArea.prop( 'scrollHeight' ) );
				}
			}
			else if ( drawType === 'prepend' ) {
				talkToWebsite.$chatMessagesArea.prepend( messageHTML );
				if ( autoScroll === true ) {
					talkToWebsite.$chatMessagesArea.scrollTop( 0 );
				}
			}

			// -------------------------------------

			// Subscribe message events
			let $renderedMessage = talkToWebsite.$chatMessagesArea.find( `[data-role="ttw-chat-message"][data-messageguid="${chatMessage.message_guid}"]` );

			// Subscribe clicks on URls inside message
			$renderedMessage.find( 'a' ).off( 'click' ).on( 'click', function( e ) {
				let followedURL = $( this ).text();
				if ( chatMessage.id !== null && $.isNumeric( chatMessage.id ) ) {
					talkToWebsite.markURLIntoMessageAsFollowed( chatMessage.id, chatMessage.message_guid, followedURL );
				}
			} );
		},

		/**
		 * Get chat message HTML code
		 * @param {Object} messageObject
		 * @returns {string}
		 */
		getChatMessageHTML: function( messageObject ) {

			// Prepare postscriptum block
			let messageFromAIPostScriptum = '';
			if ( ( talkToWebsite.pluginSettings.robotMessagePostScriptum !== null && talkToWebsite.pluginSettings.robotMessagePostScriptum.length > 0 ) && messageObject.sender_role === 'ai' ) {
				messageFromAIPostScriptum = `<div class="ttw-message-body-ps">P.S. ${talkToWebsite.pluginSettings.robotMessagePostScriptum}</div>`;
			}

			// Prepare sender name and icon
			let messageSenderNameAndIcon = '';
			if ( messageObject.sender_role === 'user' ) {
				// Message sent by user
				messageSenderNameAndIcon = `<span>${talkToWebsite.localStorageGet( 'vappv-ttw-username' )}</span> <span class="ttw-chat-user-icon"><svg width="20px" height="20px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="24" height="24" /><path fill-rule="evenodd" clip-rule="evenodd" d="M6 8C6 4.68629 8.68629 2 12 2C15.3137 2 18 4.68629 18 8C18 11.3137 15.3137 14 12 14C8.68629 14 6 11.3137 6 8Z" fill="#323232"/><path fill-rule="evenodd" clip-rule="evenodd" d="M5.43094 16.9025C7.05587 16.2213 9.2233 16 12 16C14.771 16 16.9351 16.2204 18.5586 16.8981C20.3012 17.6255 21.3708 18.8613 21.941 20.6587C22.1528 21.3267 21.6518 22 20.9592 22H3.03459C2.34482 22 1.84679 21.3297 2.0569 20.6654C2.62537 18.8681 3.69119 17.6318 5.43094 16.9025Z" fill="#323232"/></svg></span>`;
			}
			else {
				// Message sent by AI
				messageSenderNameAndIcon = `<span class="ttw-chat-ai-icon"><svg width="20px" height="20px" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><title>ai</title><g id="Layer_2" data-name="Layer 2"><g id="invisible_box" data-name="invisible box"><rect width="48" height="48" fill="none"></rect></g><g id="Q3_icons" data-name="Q3 icons"><g><path d="M45.6,18.7,41,14.9V7.5a1,1,0,0,0-.6-.9L30.5,2.1h-.4l-.6.2L24,5.9,18.5,2.2,17.9,2h-.4L7.6,6.6a1,1,0,0,0-.6.9v7.4L2.4,18.7a.8.8,0,0,0-.4.8v9H2a.8.8,0,0,0,.4.8L7,33.1v7.4a1,1,0,0,0,.6.9l9.9,4.5h.4l.6-.2L24,42.1l5.5,3.7.6.2h.4l9.9-4.5a1,1,0,0,0,.6-.9V33.1l4.6-3.8a.8.8,0,0,0,.4-.7V19.4h0A.8.8,0,0,0,45.6,18.7Zm-5.1,6.8H42v1.6l-3.5,2.8-.4.3-.4-.2a1.4,1.4,0,0,0-2,.7,1.5,1.5,0,0,0,.6,2l.7.3h0v5.4l-6.6,3.1-4.2-2.8-.7-.5V25.5H27a1.5,1.5,0,0,0,0-3H25.5V9.7l.7-.5,4.2-2.8L37,9.5v5.4h0l-.7.3a1.5,1.5,0,0,0-.6,2,1.4,1.4,0,0,0,1.3.9l.7-.2.4-.2.4.3L42,20.9v1.6H40.5a1.5,1.5,0,0,0,0,3ZM21,25.5h1.5V38.3l-.7.5-4.2,2.8L11,38.5V33.1h0l.7-.3a1.5,1.5,0,0,0,.6-2,1.4,1.4,0,0,0-2-.7l-.4.2-.4-.3L6,27.1V25.5H7.5a1.5,1.5,0,0,0,0-3H6V20.9l3.5-2.8.4-.3.4.2.7.2a1.4,1.4,0,0,0,1.3-.9,1.5,1.5,0,0,0-.6-2L11,15h0V9.5l6.6-3.1,4.2,2.8.7.5V22.5H21a1.5,1.5,0,0,0,0,3Z"></path><path d="M13.9,9.9a1.8,1.8,0,0,0,0,2.2l2.6,2.5v2.8l-4,4v5.2l4,4v2.8l-2.6,2.5a1.8,1.8,0,0,0,0,2.2,1.5,1.5,0,0,0,1.1.4,1.5,1.5,0,0,0,1.1-.4l3.4-3.5V29.4l-4-4V22.6l4-4V13.4L16.1,9.9A1.8,1.8,0,0,0,13.9,9.9Z"></path><path d="M31.5,14.6l2.6-2.5a1.8,1.8,0,0,0,0-2.2,1.8,1.8,0,0,0-2.2,0l-3.4,3.5v5.2l4,4v2.8l-4,4v5.2l3.4,3.5a1.7,1.7,0,0,0,2.2,0,1.8,1.8,0,0,0,0-2.2l-2.6-2.5V30.6l4-4V21.4l-4-4Z"></path></g></g></g></svg></span> <span>${talkToWebsite.pluginSettings.robotName}</span>`;
			}

			// Prepare message block
			return `<div class="ttw-chat-message ttw-chat-message-by-${messageObject.sender_role}" data-messageguid="${messageObject.message_guid}" data-id="${messageObject.id}" data-role="ttw-chat-message" data-dialogue-id="${messageObject.dialogue_id}">
				<div class="ttw-message-body">
					<div class="ttw-message-body-text">${talkToWebsite.prepareMessageText(messageObject.message_text)}</div>
					${messageFromAIPostScriptum}	
				</div>
				<div class="ttw-message-triangle-container">
					<div class="ttw-message-triangle-cloud-background-shadow"></div>
					<div class="ttw-message-triangle-cloud-arrow"></div>
				</div>
				<div class="ttw-message-meta">
					<div class="ttw-message-meta-username">${messageSenderNameAndIcon}</div>
					<div class="ttw-message-meta-sentdate">${talkToWebsite.formatDateYYYYMMDD_comma_HHMM(new Date(messageObject.message_date))}</div>
				</div>
			</div>`;
		},

		/**
		 * Render "Waiting for AI response" block
		 */
		renderWaitingForAIResponse: function () {
			let waitingForAIResponseText = $( '#ttw-js-messages' ).attr( 'data-ttwmessages-waiting-for-ai-response' );
			let waitingForAIResponseHTMLBlock = `<div class="ttw-waiting-for-ai-response-text-block" id="ttw-waiting-for-ai-response-text-block" data-role="ttw-waiting-for-ai-response-text-block">
				<div class="ttw-waiting-for-ai-response-circle-loader">
					<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="25px" height="25px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><g transform="translate(80,50)"><g transform="rotate(0)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="1"><animateTransform attributeName="transform" type="scale" begin="-0.875s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.875s"></animate></circle></g></g><g transform="translate(71.21320343559643,71.21320343559643)"><g transform="rotate(45)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.875"><animateTransform attributeName="transform" type="scale" begin="-0.75s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.75s"></animate></circle></g></g><g transform="translate(50,80)"><g transform="rotate(90)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.75"><animateTransform attributeName="transform" type="scale" begin="-0.625s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.625s"></animate></circle></g></g><g transform="translate(28.786796564403577,71.21320343559643)"><g transform="rotate(135)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.625"><animateTransform attributeName="transform" type="scale" begin="-0.5s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.5s"></animate></circle></g></g><g transform="translate(20,50.00000000000001)"><g transform="rotate(180)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.5"><animateTransform attributeName="transform" type="scale" begin="-0.375s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.375s"></animate></circle></g></g><g transform="translate(28.78679656440357,28.786796564403577)"><g transform="rotate(225)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.375"><animateTransform attributeName="transform" type="scale" begin="-0.25s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.25s"></animate></circle></g></g><g transform="translate(49.99999999999999,20)"><g transform="rotate(270)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.25"><animateTransform attributeName="transform" type="scale" begin="-0.125s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.125s"></animate></circle></g></g><g transform="translate(71.21320343559643,28.78679656440357)"><g transform="rotate(315)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.125"><animateTransform attributeName="transform" type="scale" begin="0s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="0s"></animate></circle></g></g><!-- [ldio] generated by https://loading.io/ --></svg>
				</div>
				<div class="ttw-waiting-for-ai-response-label-text">${waitingForAIResponseText}</div>
			</div>`;
			talkToWebsite.$chatMessagesArea.append( waitingForAIResponseHTMLBlock );
			talkToWebsite.$chatMessagesArea.scrollTop( talkToWebsite.$chatMessagesArea.prop( 'scrollHeight' ) );
		},

		/**
		 * Render "Loading messages" HTML block to the chat
		 * @param {string} position Message position = { "before" | "after" }
		 */
		renderLoadingMessages: function( position ) {
			let loadingMessagesText = $( '#ttw-js-messages' ).attr( 'data-ttwmessages-loading-messages' );
			let waitingForAIResponseHTMLBlock = `<div class="ttw-loading-user-messages-text-block ${(position === 'before' ? 'ttw-loader-before-chat' : '')}" id="ttw-loading-user-messages-text-block" data-role="ttw-loading-user-messages-text-block">
				<div class="ttw-loading-admin-messages-circle-loader">
					<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="25px" height="25px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><g transform="translate(80,50)"><g transform="rotate(0)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="1"><animateTransform attributeName="transform" type="scale" begin="-0.875s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.875s"></animate></circle></g></g><g transform="translate(71.21320343559643,71.21320343559643)"><g transform="rotate(45)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.875"><animateTransform attributeName="transform" type="scale" begin="-0.75s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.75s"></animate></circle></g></g><g transform="translate(50,80)"><g transform="rotate(90)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.75"><animateTransform attributeName="transform" type="scale" begin="-0.625s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.625s"></animate></circle></g></g><g transform="translate(28.786796564403577,71.21320343559643)"><g transform="rotate(135)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.625"><animateTransform attributeName="transform" type="scale" begin="-0.5s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.5s"></animate></circle></g></g><g transform="translate(20,50.00000000000001)"><g transform="rotate(180)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.5"><animateTransform attributeName="transform" type="scale" begin="-0.375s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.375s"></animate></circle></g></g><g transform="translate(28.78679656440357,28.786796564403577)"><g transform="rotate(225)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.375"><animateTransform attributeName="transform" type="scale" begin="-0.25s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.25s"></animate></circle></g></g><g transform="translate(49.99999999999999,20)"><g transform="rotate(270)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.25"><animateTransform attributeName="transform" type="scale" begin="-0.125s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.125s"></animate></circle></g></g><g transform="translate(71.21320343559643,28.78679656440357)"><g transform="rotate(315)"><circle cx="0" cy="0" r="6" fill="#1d3f72" fill-opacity="0.125"><animateTransform attributeName="transform" type="scale" begin="0s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform><animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="0s"></animate></circle></g></g><!-- [ldio] generated by https://loading.io/ --></svg>
				</div>
				<div class="ttw-loading-admin-messages-label-text">${loadingMessagesText}</div>
			</div>`;

			if ( typeof position === 'undefined' || position === null || position === 'after' ) {
				talkToWebsite.$chatMessagesArea.append( waitingForAIResponseHTMLBlock );
				talkToWebsite.$chatMessagesArea.scrollTop( talkToWebsite.$chatMessagesArea.prop( 'scrollHeight' ) );
			}
			else if ( 'before' === position ) {
				talkToWebsite.$chatMessagesArea.prepend( waitingForAIResponseHTMLBlock );
				talkToWebsite.$chatMessagesArea.scrollTop( 1 );
			}
		},

		/**
		 * Render 'Welcome to the chat' invitation message
		 */
		renderWelcomeToTheChatMessage: function () {

			// Get the default message text, and overwrite it if the text defined by the user
			let welcomeMessageText = $( '#ttw-js-messages' ).attr( 'data-ttwmessages-welcome-message-default' );
			if (
				$( '#ttw-settings-welcome-message-html' ).val() !== null &&
				$( '#ttw-settings-welcome-message-html' ).val().trim().length > 0
			) {
				welcomeMessageText = talkToWebsite.prepareMessageText( $( '#ttw-settings-welcome-message-html' ).val() );
			}

			talkToWebsite.renderChatMessage( {
				id: -1,
				message_guid: talkToWebsite.generateUUIDv4(),
				message_text: welcomeMessageText,
				dialogue_id: -1,
				message_date: new Date().toString(),
				sender_role: 'ai',
			}, 'prepend', true );
		},

		/**
		 * Display notification modal (error)
		 * @param {string} errorText
		 */
		displayError: function( errorText ) {

			// Create modal
			let closeModalButtonText = $( '#ttw-js-messages' ).attr( 'data-ttwmessages-close' );
			let modalGUID = talkToWebsite.generateUUIDv4();
			let modalWindowHTML = `<div class="ttw-notification-container" data-guid="${modalGUID}" style="display: none;"><div class="ttw-overlay"></div><div class="ttw-notification ttw-notification-error"><div class="ttw-notification-text">${errorText}</div><div class="ttw-notification-buttons"><div class="ttw-button ttw-button-mini" data-role="ttw-notification-ok">${closeModalButtonText}</div></div></div></div>`;
			$( 'body' ).append(modalWindowHTML);

			// Get links
			let $modalWindowContainer = $( 'body' ).find( `.ttw-notification-container[data-guid='${modalGUID}']` );
			let $modalWindowContentBlock = $modalWindowContainer.find( '.ttw-notification' );

			// Show modal
			$modalWindowContainer.fadeIn( 200 );

			// Adjust modal position instantly and in resizing
			talkToWebsite.adjustModalPositioning( modalGUID );
			$( window ).on( 'resize', function( e ) {
				talkToWebsite.adjustModalPositioning( modalGUID );
			} );

			// Subscribe buttons
			$modalWindowContentBlock.find( '[data-role="ttw-notification-ok"]').off( 'click' ).on( 'click', function( e ) {
				$modalWindowContainer.remove();
			} );

		},

		/**
		 * Adjust opened modal window positioning
		 * @param {string} modalGUID
		 */
		adjustModalPositioning: function( modalGUID ) {

			// Get links
			let $modalWindowContainer = $( 'body' ).find( `.ttw-notification-container[data-guid="${modalGUID}"]` );
			let $modalWindowContentBlock = $modalWindowContainer.find( '.ttw-notification' );
			let $modalWindowOverlay = $modalWindowContainer.find( '.ttw-overlay' );

			// Adjust sizing
			$modalWindowOverlay.css( {
				'top': 0,
				'height': '100%',
				'left': 0,
				'width': '100%'
			} );

			// Set content positioning
			let elementHeight = $modalWindowContentBlock.height();
			let elementWidth = $modalWindowContentBlock.width();
			let elementHeightHalf = elementHeight / 2;
			let elementWidthHalf = elementWidth / 2;
			$modalWindowContentBlock.css( {
				'top': `calc(50% - ${elementHeightHalf}px - 1rem)`,
				'left': `calc(50% - ${elementWidthHalf}px - 1rem)` }
			);
		}
	};

	window.talkToWebsite = talkToWebsite;

	// Initialize plugin after page loaded
	$( function() {
		talkToWebsite.initializePlugin();
	} );

} )( jQuery );