class Filters {
	static types = {
		Empty: {
			name: tr("Is empty"),
			description: tr("Matches if the data is empty or contains no values."),
			class: ["string", "array"],
			need_value: false
		},
		Starts: {
			name: tr("Starts with"),
			description: tr("Matches if the data starts with the specified value. Suitable for records where the data starts with certain characters or text."),
			class: "string",
			need_value: true
		},
		Ends: {
			name: tr("Ends with"),
			description: tr("Matches if the data ends with the specified value. Used to find records where the data ends with the given sequence of characters."),
			class: "string",
			need_value: true
		},
		Contains: {
			name: tr("Contains"),
			description: tr("Matches if the data contains the specified value. Useful for searching records that include particular characters or text fragments."),
			class: ["string", "array"],
			need_value: true
		},
		Mask: {
			name: tr("Matches the mask"),
			description: tr("Matches if the data follows the specified mask. Use \"*\" to match any number of characters and \"?\" to match a single character."),
			class: "string",
			need_value: true
		},
		Regexp: {
			name: tr("Matches regular expression"),
			description: tr("Matches if the data conforms to the specified regular expression pattern. Enables complex checks on string data."),
			class: "string",
			need_value: true
		},
		Equals: {
			name: tr("Equals"),
			description: tr("Matches if the data exactly matches the specified value. Suitable for precise comparisons of strings or numbers."),
			class: ["string", "int", "array"],
			need_value: true
		},
		List: {
			name: tr("Contained in list"),
			description: tr("Matches if the data is included in the predefined list of values. Useful for verifying if data belongs to a set of acceptable values."),
			class: "string",
			need_value: true
		},
		Greater: {
			name: tr("Greater"),
			description: tr("Matches if the data is greater than the specified value. Applied to numerical values such as sizes or timestamps."),
			class: "int",
			need_value: true
		},
		Less: {
			name: tr("Less"),
			description: tr("Matches if the data is less than the specified value. Used for numerical data that should be smaller than the given value."),
			class: "int",
			need_value: true
		},
		GreaterOrEqual: {
			name: tr("Greater or equal"),
			description: tr("Matches if the data is greater than or equal to the specified value. Suitable for numerical values or dates that must meet or exceed a certain threshold."),
			class: "int",
			need_value: true
		},
		LessOrEqual: {
			name: tr("Less or equal"),
			description: tr("Matches if the data is less than or equal to the specified value. Applied to numerical data or dates that must be less than or equal to the given value."),
			class: "int",
			need_value: true
		},
		True: {
			name: tr("Is true"),
			description: tr("Matches if the data evaluates to true (e.g., boolean true or similar values). Used to filter positive or enabled states."),
			class: "bool",
			need_value: false
		},
		False: {
			name: tr("Is false"),
			description: tr("Matches if the data evaluates to false (e.g., boolean false or similar values). Applied to filter negative or disabled states."),
			class: "bool",
			need_value: false
		},
		GreaterThan: {
			name: tr("Later than"),
			description: tr("Matches if the data is later than the specified date. Applied to filter records with timestamps after the given date."),
			class: "date",
			need_value: true
		},
		LessThan: {
			name: tr("Earlier than"),
			description: tr("Matches if the data is earlier than the specified date. Used for filtering records with timestamps before the specified date."),
			class: "date",
			need_value: true
		},
		SameYear: {
			name: tr("Same year"),
			description: tr("Matches if the data is from the same year as the specified date. Suitable for filtering records that belong to the same year."),
			class: "date",
			need_value: true
		},
		SameMonth: {
			name: tr("Same month"),
			description: tr("Matches if the data is from the same month as the specified date. Useful for finding records from a particular month."),
			class: "date",
			need_value: true
		},
		SameDay: {
			name: tr("Same day"),
			description: tr("Matches if the data is from the same day as the specified date. Used for filtering records by a specific day."),
			class: "date",
			need_value: true
		},
		SameHour: {
			name: tr("Same hour"),
			description: tr("Matches if the data is from the same hour as the specified date. Applied to precisely match records based on time."),
			class: "date",
			need_value: true
		}
	};

	prepareFields(fields){
		return fields.map((fieldData, index) => {
			let attrs = {};
			if(fieldData.types){
				attrs.types = fieldData.types.toString();
			}else{
				attrs.class = (fieldData.class || "string");
				if(fieldData.exclude)
					attrs.exclude = fieldData.exclude.toString();
			}

			return {
				value: fieldData.code,
				selected: index === 0,
				name: (fieldData.name || fieldData.code),
				title: fieldData.description,
				data: attrs
			}
		});
	}

	prepareFieldExamples(fields){
		return fields.map((fieldData) => {
			return {code: (fieldData.name || fieldData.code), description: fieldData.description}
		}).filter((fieldData) => {
			return typeof fieldData.code === "string" && typeof fieldData.description === "string";
		});
	}

	getAvailableTypes(fields = [{ class: "string" }]){
		let types = new Set();
		for(let fieldData of fields){
			if(fieldData.types){
				if(typeof fieldData.types == "string")
					fieldData.types = fieldData.types.split(',');
				fieldData.types.forEach(types.add, types);
				continue;
			}

			for(let key in this.constructor.types){
				if(fieldData.exclude){
					if(typeof fieldData.exclude == "string")
						fieldData.exclude = fieldData.exclude.split(',');
					if(fieldData.exclude.includes(key))
						continue;
				}

				let typeClass = this.constructor.types[key].class;
				if(!Array.isArray(typeClass))
					typeClass = [typeClass];
				if(typeClass.includes(fieldData.class))
					types.add(key);
			}
		}
		return Array.from(types);
	}

	getTypeData(type){
		return (typeof type == "object"
			? Object.assign({}, this.constructor.types[type.code], type)
			: Object.assign({code: type}, this.constructor.types[type])
		);
	}

	prepareTypes(types){
		return types.map((type, index) => {
			let typeData = this.getTypeData(type);
			return {
				value: typeData.code,
				selected: index === 0,
				name: (typeData.name || typeData.code),
				title: typeData.description,
				data: {
					"class": (typeData.class.toString() || "string"),
					"need-value": ("need_value" in typeData ? typeData.need_value : true)
				}
			}
		});
	}

	prepareTypeExamples(types){
		return types.map((type) => {
			let typeData = this.getTypeData(type);
			return {code: (typeData.name || typeData.code), description: typeData.description}
		}).filter((typeData) => {
			return typeof typeData.code === "string" && typeof typeData.description === "string";
		});
	}

	onFieldChange(containerID){
		let fieldData = $(`#filter-field-${containerID}`).children("option:selected").data();
		let first = true;
		if(typeof fieldData.types == "string")
			fieldData.types = fieldData.types.split(',');
		if(typeof fieldData.exclude == "string")
			fieldData.exclude = fieldData.exclude.split(',');
		let titleEll = $(`span[data-preserve-id=filter-type-${containerID}]`).next().children("i");
		let titleContent = $(titleEll.data('original-title'));

		SelectSetData(`filter-type-${containerID}`, SelectGetData(`filter-type-${containerID}`).map((typeOption) => {
			let typeClass = typeOption.data.class;
			if(typeof typeClass == "string")
				typeClass = typeClass.split(',');

			let visible = fieldData.types ?
				fieldData.types.includes(typeOption.value) :
				typeClass.includes(fieldData.class) && (!fieldData.exclude || !fieldData.exclude.includes(typeOption.value));

			typeOption.selected = visible && first;
			first = !visible ? first : false;
			typeOption.display = visible;

			titleContent.find(".help-popup-example").filter(function(){
				return $(this).find('.help-popup-code').text().trim() === typeOption.text;
			}).toggle(visible);

			return typeOption;
		}));

		titleEll.attr('data-original-title', titleContent.prop('outerHTML')).tooltip('fixTitle');
	}

	onTypeChange(containerID){
		$(`#filter-value-box-${containerID}`).toggle($(`#filter-type-${containerID}`).children("option:selected").data("need-value"));
	}

	openModal(containerID){
		let modalInstance = $(`#filter-modal-${containerID}`);
		modalInstance.modal("show");
		$(".modal-backdrop").last().css("z-index", "9998");
	}

	closeModal(containerID){
		$(`#filter-modal-${containerID}`).modal("hide");
		this.unlockButtons(containerID);
	}

	openConstructor(containerID){
		if($(`#filter-mode-${containerID}`).length)
			SelectChange(`filter-mode-${containerID}`, "inclusive");

		if($(`#filter-field-${containerID}`).length)
			SelectChange(`filter-field-${containerID}`, $(`#filter-field-${containerID}`).children("option:first").val());
		else
			SelectChange(`filter-type-${containerID}`, $(`#filter-type-${containerID}`).children("option:first").val());

		$(`*[data-preserve-id=filter-value-${containerID}] a.string`).click();
		$(`#filter-value-${containerID}`).val("").trigger("change");

		$(`#filter-remove-${containerID}`)
			.attr("onclick", "")
			.hide();

		$(`#filter-accept-${containerID}`)
			.text(tr("Add filter"))
			.attr("onclick", `_Filters.create('${containerID}')`);

		this.openModal(containerID);
	}

	getNextFilterID(containerID){
		let filterID = 1;
		$(`.filter-list[id=${containerID}]`).children().map(function(){
			filterID = Math.max(filterID, $(this).data("id") + 1);
		});
		return filterID;
	}

	getFieldName(containerID, field){
		return $(`#filter-field-${containerID}`).children(`option[value=${field}]`).text() || field;
	}

	getTypeName(containerID, type){
		return $(`#filter-type-${containerID}`).children(`option[value=${type}]`).text() || type;
	}

	createFilterItem(containerID, filterData, filterID){
		let filterEll = $("<button type='button'></button>")
			.addClass("filter-item").attr({
				"data-id": filterID,
				"title": tr("Click to edit or remove this filter")
			}).val(JSON.stringify(filterData));

		if("match" in filterData && !filterData.match)
			filterEll.addClass("negative-filter");

		let title = "";
		if(filterData.field){
			title += this.getFieldName(containerID, filterData.field);
			title += " ";
		}
		title += this.getTypeName(containerID, filterData.type);

		filterEll.append(`<span class='filter-item-title'>${_.escape(title)}</span>`);

		if(filterData.value)
			filterEll.append(`<span class='filter-item-value'>${_.escape(filterData.value.data)}</span>`);

		filterEll.on("dblclick", (event) => {
			event.preventDefault();
			event.stopPropagation();
			this.openEdit(containerID, filterID);
		});

		filterEll.on("contextmenu click", (event) => this.handleContextMenu(event, containerID, filterID));

		return filterEll;
	}

	handleContextMenu(event, containerID, filterID)
	{
		if(event.eventPhase != 3)
			return;

		event.preventDefault();
		event.stopPropagation();

		let menu = $(`#filter-contextmenu-${containerID}`);
		menu.find(".filter-edit").attr("onclick", `_Filters.openEdit('${containerID}', ${filterID});return false`);
		menu.find(".filter-remove").attr("onclick", `_Filters.askRemove('${containerID}', ${filterID});return false`);

		menu.css({
			display: "block",
			left: `${event.pageX}px`,
			top: `${event.pageY}px`
		});

		$(document).one("click", () => menu.hide());
	}

	toggleAdditional(containerID){
		let filterList = $(`.filter-list[id=${containerID}]`);
		let filterCount = filterList.children().length;
		let sortableInstance = filterList.data("sortable");

		if(filterCount > 1){
			if(!sortableInstance){
				filterList.data("sortable", Sortable.create(filterList[0], {
					animation: 70,
					ghostClass: "filter-drag",
					onStart: () => {
						$(".filter-contextmenu").hide();
					}
				}));
			}
		}else if(sortableInstance){
			sortableInstance.destroy();
			filterList.removeData("sortable");
		}

		let filterLogic = $(`#filter-logic-${containerID}`);
		if(filterLogic.length)
			filterLogic.toggle(filterCount > 1);
	}

	addFilter(containerID, filterData, filterID = this.getNextFilterID(containerID)){
		let filterEll = this.createFilterItem(containerID, filterData, filterID);
		$(`.filter-list[id=${containerID}]`).append(filterEll);
		this.toggleAdditional(containerID);
	}

	extractFilterValue(containerID){
		let loader = new ResourceLoader();
		let value = GetInputConstructorValue(`filter-value-${containerID}`, loader);
		return {
			data: value.original,
			class: $(`*[data-selector-id=filter-value-${containerID}]`).html()
		};
	}

	collectFilterData(containerID){
		let filterData = {
			field: $(`#filter-field-${containerID}`).val(),
			type: $(`#filter-type-${containerID}`).val()
		};
		if($(`#filter-mode-${containerID}`).length)
			filterData.match = $(`#filter-mode-${containerID}`).val() === "inclusive";
		if($(`#filter-type-${containerID}`).children("option:selected").data("need-value"))
			filterData.value = this.extractFilterValue(containerID);
		return filterData;
	}

	isDuplicateFilter(containerID, filterData, filterID = null){
		let filterStr = JSON.stringify(filterData);
		let isDuplicate = false;

		$(`.filter-list[id=${containerID}]`).children().map(function(){
			if(filterID !== null && $(this).data("id") == filterID)
				return;

			if($(this).val() === filterStr){
				bootbox.alert(tr("Such a filter already exists"));
				isDuplicate = true;
				return false;
			}
		});

		return isDuplicate;
	}

	lockButtons(containerID, lock = true){
		$(`#filter-remove-${containerID}`).prop("disabled", lock);
		$(`#filter-accept-${containerID}`).prop("disabled", lock);
	}

	unlockButtons(containerID){
		this.lockButtons(containerID, false);
	}

	create(containerID){
		this.lockButtons(containerID);
		let filterData = this.collectFilterData(containerID);
		if(this.isDuplicateFilter(containerID, filterData))
			return this.unlockButtons(containerID);

		this.addFilter(containerID, filterData);
		this.closeModal(containerID);
	}

	createChangeText(name, oldValue, newValue) {
		return `<strong>${_.escape(name)}:</strong> <em style="color: #808080;">${_.escape(oldValue)}</em> <strong>→</strong> <em style="color: #28a745;">${_.escape(newValue)}</em>`;
	}

	compareFilters(containerID, oldFilter, newFilter){
		let changes = [];

		if(oldFilter.match !== newFilter.match){
			let oldMatch = oldFilter.match ? tr("Inclusive") : tr("Exclusive");
			let newMatch = newFilter.match ? tr("Inclusive") : tr("Exclusive");
			changes.push(this.createChangeText(tr("Filter type"), oldMatch, newMatch));
		}

		if(oldFilter.field !== newFilter.field){
			let oldField = this.getFieldName(containerID, oldFilter.field);
			let newField = this.getFieldName(containerID, newFilter.field);
			changes.push(this.createChangeText(tr("Filter field"), oldField, newField));
		}
			

		if(oldFilter.type !== newFilter.type){
			let oldType = this.getTypeName(containerID, oldFilter.type);
			let newType = this.getTypeName(containerID, newFilter.type);
			changes.push(this.createChangeText(tr("Filtering method"), oldType, newType));
		}

		if(
			(oldFilter.value && !newFilter.value) ||
			(!oldFilter.value && newFilter.value) ||
			(oldFilter.value && newFilter.value && oldFilter.value.data !== newFilter.value.data)
		){
			let oldValueText = oldFilter.value ? oldFilter.value.data : tr("No value");
			let newValueText = newFilter.value ? newFilter.value.data : tr("No value");
			changes.push(this.createChangeText(tr("Value"), oldValueText, newValueText));
		}

		if(oldFilter.value && newFilter.value && oldFilter.value.class !== newFilter.value.class){
			changes.push(this.createChangeText(tr("Value class"), oldFilter.value.class, newFilter.value.class));
		}

		return changes;
	}

	findFilter(containerID, filterID){
		return $(`.filter-list[id=${containerID}]`).children(`.filter-item[data-id=${filterID}]`);
	}

	updateFilter(containerID, filterData, filterID){
		let filterEll = this.createFilterItem(containerID, filterData, filterID);
		this.findFilter(containerID, filterID).replaceWith(filterEll);
	}

	edit(containerID, filterID){
		this.lockButtons(containerID);
		let newFilterData = this.collectFilterData(containerID);
		if(this.isDuplicateFilter(containerID, newFilterData, filterID))
			return this.unlockButtons(containerID);

		let oldFilterData = JSON.parse(this.findFilter(containerID, filterID).val());
		let changes = this.compareFilters(containerID, oldFilterData, newFilterData);
		if(changes.length > 0){
			bootbox.confirm({
				message: `
					<div style="text-align: center; word-break: break-all;">
						${tr("Are you sure you want to change the filter?")}<br><br>
						${changes.join("<br>")}
					</div>
				`,
				buttons: {
					confirm: {
						label: tr("Confirm Changes"),
						className: "btn-success"
					},
					cancel: {
						label: tr("Cancel"),
						className: "btn-default"
					}
				},
				callback: (res) => {
					if(res){
						this.updateFilter(containerID, newFilterData, filterID);
						this.closeModal(containerID);
					}else{
						this.unlockButtons(containerID);
					}
				}
			});
		}else{
			this.closeModal(containerID);
		}
	}

	openEdit(containerID, filterID){
		let filterData = JSON.parse(this.findFilter(containerID, filterID).val());

		if("match" in filterData)
			SelectChange(`filter-mode-${containerID}`, filterData.match ? "inclusive" : "exclusive");
		if(filterData.field)
			SelectChange(`filter-field-${containerID}`, filterData.field);
		SelectChange(`filter-type-${containerID}`, filterData.type);

		if(filterData.value){
			$(`*[data-preserve-id=filter-value-${containerID}] a.${filterData.value.class}`).click();

			if(filterData.value.class == "int")
				$(`#filter-value-${containerID}_number`).val(parseInt(filterData.value.data)).trigger("change");
			else
				$(`#filter-value-${containerID}`).val(filterData.value.data).trigger("change");
		}else{
			$(`*[data-preserve-id=filter-value-${containerID}] a.string`).click();
			$(`#filter-value-${containerID}`).val("").trigger("change");
		}

		$(`#filter-remove-${containerID}`)
			.attr("onclick", `_Filters.askRemove('${containerID}', ${filterID}, true)`)
			.show();

		$(`#filter-accept-${containerID}`)
			.text(tr("Edit filter"))
			.attr("onclick", `_Filters.edit('${containerID}', ${filterID})`);

		this.openModal(containerID);
	}

	clearFilters(containerID){
		$(`.filter-list[id=${containerID}]`).empty();
		this.toggleAdditional(containerID);
	}

	askClear(containerID){
		bootbox.confirm(tr("Remove all filters?"), (res) => {
			if(res) this.clearFilters(containerID);
		});
	}

	removeFilter(containerID, filterID){
		this.findFilter(containerID, filterID).remove();
		this.toggleAdditional(containerID);
	}

	askRemove(containerID, filterID, closeModal = false){
		bootbox.confirm(tr("Remove this filter?"), (res) => {
			if(res){
				this.removeFilter(containerID, filterID);
				if(closeModal)
					this.closeModal(containerID);
			}
		});
	}

	restore(containerID, filters){
		this.clearFilters(containerID);

		if(filters.length === 0)
			return;

		filters.forEach((filterData, index) => {
			this.addFilter(containerID, filterData, index + 1);
		});

		let filterCheck = $(`#Filter${containerID}Check`);
		if(filterCheck.length)
			filterCheck.prop("checked", true).trigger("change");
	}

	isEmpty(containerID){
		return $(`.filter-list[id=${containerID}]`).children().length === 0;
	}

	getFilters(containerID){
		return $(`.filter-list[id=${containerID}]`).children().map(function(){
			return JSON.parse($(this).val());
		}).get();
	}

	getFiltersValue(containerID){
		let filters = this.getFilters(containerID);
		let loader = new ResourceLoader();

		let filterStrings = filters.map(function(filterData){
			let filterParts = [];

			if("match" in filterData)
				filterParts.push(`match:${filterData.match}`);

			if(filterData.field)
				filterParts.push(`field:"${filterData.field}"`);

			filterParts.push(`type:"${filterData.type}"`);

			if(filterData.value){
				loader.SetIsString(filterData.value.class == "string");
				filterParts.push(`value:(${filterData.value.data.length ? loader.Resolve(filterData.value.data + "") : '""'})`);
			}	

			return `{${filterParts.join(",")}}`;
		});

		let filtersArray = `[${filterStrings.join(",")}]`;
		if(!$(`#filter-logic-${containerID}`).length)
			return filtersArray;

		let isOrLogic = $(`#filter-logic-select-${containerID}`).val() == "or";
		return `{or:${isOrLogic},data:${filtersArray}}`;
	}
}