import { Inject, Injectable } from '@angular/core';
import { config, IConfig } from './config';

@Injectable()
export class MaskApplierService {
	public dropSpecialCharacters: IConfig['dropSpecialCharacters'];

	public hiddenInput: IConfig['hiddenInput'];

	public showTemplate!: IConfig['showTemplate'];

	public clearIfNotMatch!: IConfig['clearIfNotMatch'];

	public maskExpression: string = '';

	public actualValue: string = '';

	public shownMaskExpression: string = '';

	public maskSpecialCharacters!: IConfig['specialCharacters'];

	public maskAvailablePatterns!: IConfig['patterns'];

	public prefix!: IConfig['prefix'];

	public suffix!: IConfig['suffix'];

	public thousandSeparator!: IConfig['thousandSeparator'];

	public decimalMarker!: IConfig['decimalMarker'];

	public customPattern!: IConfig['patterns'];

	public ipError?: boolean;

	public cpfCnpjError?: boolean;

	public showMaskTyped!: IConfig['showMaskTyped'];

	public placeHolderCharacter!: IConfig['placeHolderCharacter'];

	public validation: IConfig['validation'];

	public separatorLimit: IConfig['separatorLimit'];

	public allowNegativeNumbers: IConfig['allowNegativeNumbers'];

	public leadZeroDateTime: IConfig['leadZeroDateTime'];

	private _shift!: Set<number>;

	public constructor(@Inject(config) protected _config: IConfig) {
		this._shift = new Set();
		this.clearIfNotMatch = this._config.clearIfNotMatch;
		this.dropSpecialCharacters = this._config.dropSpecialCharacters;
		this.maskSpecialCharacters = this._config.specialCharacters;
		this.maskAvailablePatterns = this._config.patterns;
		this.prefix = this._config.prefix;
		this.suffix = this._config.suffix;
		this.thousandSeparator = this._config.thousandSeparator;
		this.decimalMarker = this._config.decimalMarker;
		this.hiddenInput = this._config.hiddenInput;
		this.showMaskTyped = this._config.showMaskTyped;
		this.placeHolderCharacter = this._config.placeHolderCharacter;
		this.validation = this._config.validation;
		this.separatorLimit = this._config.separatorLimit;
		this.allowNegativeNumbers = this._config.allowNegativeNumbers;
		this.leadZeroDateTime = this._config.leadZeroDateTime;
	}

	public applyMaskWithPattern(
		inputValue: string,
		maskAndPattern: [string, IConfig['patterns']],
	): string {
		const [mask, customPattern] = maskAndPattern;
		this.customPattern = customPattern;
		return this.applyMask(inputValue, mask);
	}

	public applyMask(
		inputValue: string | object | boolean | null | undefined,
		maskExpression: string,
		position: number = 0,
		justPasted: boolean = false,
		backspaced: boolean = false,
		cb: Function = () => {},
	): string {
		if (!maskExpression || typeof inputValue !== 'string') {
			return '';
		}
		let cursor = 0;
		let result = '';
		let multi = false;
		let backspaceShift = false;
		let shift = 1;
		let stepBack = false;
		if (inputValue.slice(0, this.prefix.length) === this.prefix) {
			// eslint-disable-next-line no-param-reassign
			inputValue = inputValue.slice(this.prefix.length, inputValue.length);
		}
		if (!!this.suffix && inputValue?.length > 0) {
			// eslint-disable-next-line no-param-reassign
			inputValue = this.checkAndRemoveSuffix(inputValue);
		}
		const inputArray: string[] = inputValue.toString().split('');
		if (maskExpression === 'IP') {
			const valuesIP = inputValue.split('.');
			this.ipError = this._validIP(valuesIP);
			// eslint-disable-next-line no-param-reassign
			maskExpression = '099.099.099.099';
		}
		const arr: string[] = [];
		for (let i = 0; i < inputValue.length; i++) {
			if (inputValue[i]?.match('\\d')) {
				arr.push(inputValue[i]!);
			}
		}
		if (maskExpression === 'CPF_CNPJ') {
			this.cpfCnpjError = arr.length !== 11 && arr.length !== 14;
			if (arr.length > 11) {
				// eslint-disable-next-line no-param-reassign
				maskExpression = '00.000.000/0000-00';
			} else {
				// eslint-disable-next-line no-param-reassign
				maskExpression = '000.000.000-00';
			}
		}
		if (maskExpression.startsWith('percent')) {
			if (
				inputValue.match('[a-z]|[A-Z]') ||
				inputValue.match(/[-!$%^&*()_+|~=`{}\[\]:";'<>?,\/.]/)
			) {
				// eslint-disable-next-line no-param-reassign
				inputValue = this._stripToDecimal(inputValue);
				const precision: number = this.getPrecision(maskExpression);
				// eslint-disable-next-line no-param-reassign
				inputValue = this.checkInputPrecision(inputValue, precision, this.decimalMarker);
			}
			if (
				inputValue.indexOf('.') > 0 &&
				!this.percentage(inputValue.substring(0, inputValue.indexOf('.')))
			) {
				const base: string = inputValue.substring(0, inputValue.indexOf('.') - 1);
				// eslint-disable-next-line no-param-reassign
				inputValue = `${base}${inputValue.substring(inputValue.indexOf('.'), inputValue.length)}`;
			}
			if (this.percentage(inputValue)) {
				result = inputValue;
			} else {
				result = inputValue.substring(0, inputValue.length - 1);
			}
		} else if (maskExpression.startsWith('separator')) {
			if (
				inputValue.match('[wа-яА-Я]') ||
				inputValue.match('[ЁёА-я]') ||
				inputValue.match('[a-z]|[A-Z]') ||
				inputValue.match(/[-@#!$%\\^&*()_£¬'+|~=`{}\[\]:";<>.?\/]/) ||
				inputValue.match('[^A-Za-z0-9,]')
			) {
				// eslint-disable-next-line no-param-reassign
				inputValue = this._stripToDecimal(inputValue);
			}

			// eslint-disable-next-line no-param-reassign
			inputValue =
				inputValue.length > 1 &&
				inputValue[0] === '0' &&
				inputValue[1] !== this.thousandSeparator &&
				!this._compareOrIncludes(inputValue[1], this.decimalMarker, this.thousandSeparator) &&
				!backspaced
					? inputValue.slice(0, inputValue.length - 1)
					: inputValue;

			if (backspaced) {
				// eslint-disable-next-line no-param-reassign
				inputValue = this._compareOrIncludes(
					inputValue[inputValue.length - 1],
					this.decimalMarker,
					this.thousandSeparator,
				)
					? inputValue.slice(0, inputValue.length - 1)
					: inputValue;
			}
			// TODO: we had different rexexps here for the different cases... but tests dont seam to bother - check this
			//  separator: no COMMA, dot-sep: no SPACE, COMMA OK, comma-sep: no SPACE, COMMA OK

			const thousandSeparatorCharEscaped: string = this._charToRegExpExpression(
				this.thousandSeparator,
			);
			let invalidChars: string = '@#!$%^&*()_+|~=`{}\\[\\]:\\s,\\.";<>?\\/'.replace(
				thousandSeparatorCharEscaped,
				'',
			);
			//.replace(decimalMarkerEscaped, '');
			if (Array.isArray(this.decimalMarker)) {
				for (const marker of this.decimalMarker) {
					invalidChars = invalidChars.replace(this._charToRegExpExpression(marker), '');
				}
			} else {
				invalidChars = invalidChars.replace(this._charToRegExpExpression(this.decimalMarker), '');
			}

			const invalidCharRegexp: RegExp = new RegExp('[' + invalidChars + ']');

			if (
				inputValue.match(invalidCharRegexp) ||
				(inputValue.length === 1 &&
					this._compareOrIncludes(inputValue, this.decimalMarker, this.thousandSeparator))
			) {
				// eslint-disable-next-line no-param-reassign
				inputValue = inputValue.substring(0, inputValue.length - 1);
			}

			const precision: number = this.getPrecision(maskExpression);
			// eslint-disable-next-line no-param-reassign
			inputValue = this.checkInputPrecision(inputValue, precision, this.decimalMarker);
			const strForSep: string = inputValue.replace(
				new RegExp(thousandSeparatorCharEscaped, 'g'),
				'',
			);
			result = this._formatWithSeparators(
				strForSep,
				this.thousandSeparator,
				this.decimalMarker,
				precision,
			);
			const commaShift: number = result.indexOf(',') - inputValue.indexOf(',');
			const shiftStep: number = result.length - inputValue.length;

			if (shiftStep > 0 && result[position] !== ',') {
				backspaceShift = true;
				let _shift = 0;
				do {
					this._shift.add(position + _shift);
					_shift++;
				} while (_shift < shiftStep);
			} else if (
				(commaShift !== 0 && position > 0 && !(result.indexOf(',') >= position && position > 3)) ||
				(!(result.indexOf('.') >= position && position > 3) && shiftStep <= 0)
			) {
				this._shift.clear();
				backspaceShift = true;
				shift = shiftStep;
				// eslint-disable-next-line no-param-reassign
				position += shiftStep;
				this._shift.add(position);
			} else {
				this._shift.clear();
			}
		} else {
			for (
				// eslint-disable-next-line
				let i: number = 0, inputSymbol: string = inputArray[0]!;
				i < inputArray.length;
				i++, inputSymbol = inputArray[i]!
			) {
				if (cursor === maskExpression.length) {
					break;
				}
				if (
					this._checkSymbolMask(inputSymbol, maskExpression[cursor]!) &&
					maskExpression[cursor + 1] === '?'
				) {
					result += inputSymbol;
					cursor += 2;
				} else if (
					maskExpression[cursor + 1] === '*' &&
					multi &&
					this._checkSymbolMask(inputSymbol, maskExpression[cursor + 2]!)
				) {
					result += inputSymbol;
					cursor += 3;
					multi = false;
				} else if (
					this._checkSymbolMask(inputSymbol, maskExpression[cursor]!) &&
					maskExpression[cursor + 1] === '*'
				) {
					result += inputSymbol;
					multi = true;
				} else if (
					maskExpression[cursor + 1] === '?' &&
					this._checkSymbolMask(inputSymbol, maskExpression[cursor + 2]!)
				) {
					result += inputSymbol;
					cursor += 3;
				} else if (this._checkSymbolMask(inputSymbol, maskExpression[cursor]!)) {
					if (maskExpression[cursor] === 'H') {
						if (Number(inputSymbol) > 2) {
							cursor += 1;
							this._shiftStep(maskExpression, cursor, inputArray.length);
							i--;
							if (this.leadZeroDateTime) {
								result += '0';
							}
							continue;
						}
					}
					if (maskExpression[cursor] === 'h') {
						if (result === '2' && Number(inputSymbol) > 3) {
							cursor += 1;
							i--;
							continue;
						}
					}
					if (maskExpression[cursor] === 'm') {
						if (Number(inputSymbol) > 5) {
							cursor += 1;
							this._shiftStep(maskExpression, cursor, inputArray.length);
							i--;
							if (this.leadZeroDateTime) {
								result += '0';
							}
							continue;
						}
					}
					if (maskExpression[cursor] === 's') {
						if (Number(inputSymbol) > 5) {
							cursor += 1;
							this._shiftStep(maskExpression, cursor, inputArray.length);
							i--;
							if (this.leadZeroDateTime) {
								result += '0';
							}
							continue;
						}
					}
					const daysCount = 31;
					if (maskExpression[cursor] === 'd') {
						if (
							(Number(inputSymbol) > 3 && this.leadZeroDateTime) ||
							Number(inputValue.slice(cursor, cursor + 2)) > daysCount ||
							inputValue[cursor + 1] === '/'
						) {
							cursor += 1;
							this._shiftStep(maskExpression, cursor, inputArray.length);
							i--;
							if (this.leadZeroDateTime) {
								result += '0';
							}
							continue;
						}
					}
					if (maskExpression[cursor] === 'M') {
						const monthsCount = 12;
						// mask without day
						const withoutDays: boolean =
							cursor === 0 &&
							(Number(inputSymbol) > 2 ||
								Number(inputValue.slice(cursor, cursor + 2)) > monthsCount ||
								inputValue[cursor + 1] === '/');
						// day<10 && month<12 for input
						const day1monthInput: boolean =
							inputValue.slice(cursor - 3, cursor - 1).includes('/') &&
							((inputValue[cursor - 2] === '/' &&
								Number(inputValue.slice(cursor - 1, cursor + 1)) > monthsCount &&
								inputValue[cursor] !== '/') ||
								inputValue[cursor] === '/' ||
								(inputValue[cursor - 3] === '/' &&
									Number(inputValue.slice(cursor - 2, cursor)) > monthsCount &&
									inputValue[cursor - 1] !== '/') ||
								inputValue[cursor - 1] === '/');
						// 10<day<31 && month<12 for input
						const day2monthInput: boolean =
							Number(inputValue.slice(cursor - 3, cursor - 1)) <= daysCount &&
							!inputValue.slice(cursor - 3, cursor - 1).includes('/') &&
							inputValue[cursor - 1] === '/' &&
							(Number(inputValue.slice(cursor, cursor + 2)) > monthsCount ||
								inputValue[cursor + 1] === '/');
						// day<10 && month<12 for paste whole data
						const day1monthPaste: boolean =
							Number(inputValue.slice(cursor - 3, cursor - 1)) > daysCount &&
							!inputValue.slice(cursor - 3, cursor - 1).includes('/') &&
							!inputValue.slice(cursor - 2, cursor).includes('/') &&
							Number(inputValue.slice(cursor - 2, cursor)) > monthsCount;
						// 10<day<31 && month<12 for paste whole data
						const day2monthPaste: boolean =
							Number(inputValue.slice(cursor - 3, cursor - 1)) <= daysCount &&
							!inputValue.slice(cursor - 3, cursor - 1).includes('/') &&
							inputValue[cursor - 1] !== '/' &&
							Number(inputValue.slice(cursor - 1, cursor + 1)) > monthsCount;

						if (
							(Number(inputSymbol) > 1 && this.leadZeroDateTime) ||
							withoutDays ||
							day1monthInput ||
							day2monthInput ||
							day1monthPaste ||
							day2monthPaste
						) {
							cursor += 1;
							this._shiftStep(maskExpression, cursor, inputArray.length);
							i--;
							if (this.leadZeroDateTime) {
								result += '0';
							}
							continue;
						}
					}
					result += inputSymbol;
					cursor++;
				} else if (inputSymbol === ' ' && maskExpression[cursor] === ' ') {
					result += inputSymbol;
					cursor++;
				} else if (this.maskSpecialCharacters.indexOf(maskExpression[cursor]!) !== -1) {
					result += maskExpression[cursor];
					cursor++;
					this._shiftStep(maskExpression, cursor, inputArray.length);
					i--;
				} else if (
					this.maskSpecialCharacters.indexOf(inputSymbol) > -1 &&
					this.maskAvailablePatterns[maskExpression[cursor]!] &&
					this.maskAvailablePatterns[maskExpression[cursor]!]?.optional
				) {
					if (
						!!inputArray[cursor] &&
						maskExpression !== '099.099.099.099' &&
						maskExpression !== '000.000.000-00' &&
						maskExpression !== '00.000.000/0000-00' &&
						!maskExpression.match(/^9+\.0+$/)
					) {
						result += inputArray[cursor];
					}
					cursor++;
					i--;
				} else if (
					this.maskExpression[cursor + 1] === '*' &&
					this._findSpecialChar(this.maskExpression[cursor + 2]!) &&
					this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2] &&
					multi
				) {
					cursor += 3;
					result += inputSymbol;
				} else if (
					this.maskExpression[cursor + 1] === '?' &&
					this._findSpecialChar(this.maskExpression[cursor + 2]!) &&
					this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2] &&
					multi
				) {
					cursor += 3;
					result += inputSymbol;
				} else if (
					this.showMaskTyped &&
					this.maskSpecialCharacters.indexOf(inputSymbol) < 0 &&
					inputSymbol !== this.placeHolderCharacter
				) {
					stepBack = true;
				}
			}
		}
		if (
			result.length + 1 === maskExpression.length &&
			this.maskSpecialCharacters.indexOf(maskExpression[maskExpression.length - 1]!) !== -1
		) {
			result += maskExpression[maskExpression.length - 1];
		}

		let newPosition: number = position + 1;

		while (this._shift.has(newPosition)) {
			shift++;
			newPosition++;
		}

		let actualShift: number =
			justPasted && !maskExpression.startsWith('separator')
				? cursor
				: this._shift.has(position)
				? shift
				: 0;
		if (stepBack) {
			actualShift--;
		}

		cb(actualShift, backspaceShift);
		if (shift < 0) {
			this._shift.clear();
		}
		let onlySpecial = false;
		if (backspaced) {
			onlySpecial = inputArray.every((char) => this.maskSpecialCharacters.includes(char));
		}
		let res = `${this.prefix}${onlySpecial ? '' : result}${this.suffix}`;
		if (result.length === 0) {
			res = `${this.prefix}${result}`;
		}
		return res;
	}

	public _findSpecialChar(inputSymbol: string): undefined | string {
		return this.maskSpecialCharacters.find((val: string) => val === inputSymbol);
	}

	protected _checkSymbolMask(inputSymbol: string, maskSymbol: string): boolean {
		this.maskAvailablePatterns = this.customPattern
			? this.customPattern
			: this.maskAvailablePatterns;
		return (
			this.maskAvailablePatterns[maskSymbol]! &&
			this.maskAvailablePatterns[maskSymbol]!.pattern &&
			this.maskAvailablePatterns[maskSymbol]!.pattern.test(inputSymbol)
		);
	}

	private _formatWithSeparators = (
		str: string,
		thousandSeparatorChar: string,
		decimalChars: string | string[],
		precision: number,
	) => {
		let x: string[] = [];
		let decimalChar: string = '';
		if (Array.isArray(decimalChars)) {
			const regExp = new RegExp(
				decimalChars.map((v) => ('[\\^$.|?*+()'.indexOf(v) >= 0 ? `\\${v}` : v)).join('|'),
			);
			x = str.split(regExp);
			decimalChar = str.match(regExp)?.[0] ?? '';
		} else {
			x = str.split(decimalChars);
			decimalChar = decimalChars;
		}
		const decimals: string = x.length > 1 ? `${decimalChar}${x[1]}` : '';
		let res: string = x[0]!;
		const separatorLimit: string = this.separatorLimit.replace(/\s/g, '');
		if (separatorLimit && +separatorLimit) {
			if (res[0] === '-') {
				res = `-${res.slice(1, res.length).slice(0, separatorLimit.length)}`;
			} else {
				res = res.slice(0, separatorLimit.length);
			}
		}
		const rgx: RegExp = /(\d+)(\d{3})/;

		while (thousandSeparatorChar && rgx.test(res)) {
			res = res.replace(rgx, '$1' + thousandSeparatorChar + '$2');
		}

		if (precision === undefined) {
			return res + decimals;
		} else if (precision === 0) {
			return res;
		}
		return res + decimals.substr(0, precision + 1);
	};

	private percentage = (str: string): boolean => {
		return Number(str) >= 0 && Number(str) <= 100;
	};

	private getPrecision = (maskExpression: string): number => {
		const x: string[] = maskExpression.split('.');
		if (x.length > 1) {
			return Number(x[x.length - 1]);
		}

		return Infinity;
	};

	private checkAndRemoveSuffix = (inputValue: string): string => {
		for (let i = this.suffix?.length - 1; i >= 0; i--) {
			const substr = this.suffix.substr(i, this.suffix?.length);
			if (
				inputValue.includes(substr) &&
				(i - 1 < 0 || !inputValue.includes(this.suffix.substr(i - 1, this.suffix?.length)))
			) {
				return inputValue.replace(substr, '');
			}
		}
		return inputValue;
	};

	private checkInputPrecision = (
		inputValue: string,
		precision: number,
		decimalMarker: IConfig['decimalMarker'],
	): string => {
		if (precision < Infinity) {
			// TODO need think about decimalMarker
			if (Array.isArray(decimalMarker)) {
				const marker = decimalMarker.find((dm) => dm !== this.thousandSeparator);
				// eslint-disable-next-line no-param-reassign
				decimalMarker = marker ? marker : decimalMarker[0];
			}
			const precisionRegEx: RegExp = new RegExp(
				this._charToRegExpExpression(decimalMarker) + `\\d{${precision}}.*$`,
			);

			const precisionMatch: RegExpMatchArray | null = inputValue.match(precisionRegEx);
			if (precisionMatch && precisionMatch[0]!.length - 1 > precision) {
				const diff = precisionMatch[0]!.length - 1 - precision;
				// eslint-disable-next-line no-param-reassign
				inputValue = inputValue.substring(0, inputValue.length - diff);
			}
			if (
				precision === 0 &&
				this._compareOrIncludes(
					inputValue[inputValue.length - 1],
					decimalMarker,
					this.thousandSeparator,
				)
			) {
				// eslint-disable-next-line no-param-reassign
				inputValue = inputValue.substring(0, inputValue.length - 1);
			}
		}
		return inputValue;
	};

	private _stripToDecimal(str: string): string {
		return str
			.split('')
			.filter((i: string, idx: number) => {
				const isDecimalMarker =
					typeof this.decimalMarker === 'string'
						? i === this.decimalMarker
						: // TODO (inepipenko) use utility type
						  this.decimalMarker.includes(i as ',' | '.');
				return (
					i.match('^-?\\d') ||
					i === this.thousandSeparator ||
					isDecimalMarker ||
					(i === '-' && idx === 0 && this.allowNegativeNumbers)
				);
			})
			.join('');
	}

	private _charToRegExpExpression(char: string): string {
		// if (Array.isArray(char)) {
		// 	return char.map((v) => ('[\\^$.|?*+()'.indexOf(v) >= 0 ? `\\${v}` : v)).join('|');
		// }
		if (char) {
			const charsToEscape = '[\\^$.|?*+()';
			return char === ' ' ? '\\s' : charsToEscape.indexOf(char) >= 0 ? `\\${char}` : char;
		}
		return char;
	}

	private _shiftStep(maskExpression: string, cursor: number, inputLength: number) {
		const shiftStep: number = /[*?]/g.test(maskExpression.slice(0, cursor)) ? inputLength : cursor;
		this._shift.add(shiftStep + this.prefix.length || 0);
	}

	protected _compareOrIncludes<T>(value: T, comparedValue: T | T[], excludedValue: T): boolean {
		return Array.isArray(comparedValue)
			? comparedValue.filter((v) => v !== excludedValue).includes(value)
			: value === comparedValue;
	}

	private _validIP(valuesIP: string[]): boolean {
		return !(
			valuesIP.length === 4 &&
			!valuesIP.some((value: string, index: number) => {
				if (valuesIP.length !== index + 1) {
					return value === '' || Number(value) > 255;
				}
				return value === '' || Number(value.substring(0, 3)) > 255;
			})
		);
	}
}
