import CSSMeasurementConverter from '../css/declaration/measurement-converter/CSSMeasurementConverter.js';
import type BrowserWindow from '../window/BrowserWindow.js';
import WindowBrowserContext from '../window/WindowBrowserContext.js';
import type IMediaQueryRange from './IMediaQueryRange.js';
import type IMediaQueryRule from './IMediaQueryRule.js';
import MediaQueryTypeEnum from './MediaQueryTypeEnum.js';

/**
 * Media query this.
 */
export default class MediaQueryItem {
	public mediaTypes: MediaQueryTypeEnum[];
	public not: boolean;
	public rules: IMediaQueryRule[];
	public ranges: IMediaQueryRange[];
	private rootFontSize: string | number | null = null;
	private window: BrowserWindow;

	/**
	 * Constructor.
	 *
	 * @param options Options.
	 * @param options.window Owner window.
	 * @param [options.rootFontSize] Root font size.
	 * @param [options.mediaTypes] Media types.
	 * @param [options.not] Not.
	 * @param [options.rules] Rules.
	 * @param [options.ranges] Ranges.
	 */
	constructor(options: {
		window: BrowserWindow;
		rootFontSize?: string | number | null;
		mediaTypes?: MediaQueryTypeEnum[];
		not?: boolean;
		rules?: IMediaQueryRule[];
		ranges?: IMediaQueryRange[];
	}) {
		this.window = options.window;
		this.rootFontSize = options.rootFontSize || null;
		this.mediaTypes = options.mediaTypes || [];
		this.not = options.not || false;
		this.rules = options.rules || [];
		this.ranges = options.ranges || [];
	}

	/**
	 * Returns media string.
	 */
	public toString(): string {
		return `${this.not ? 'not ' : ''}${this.mediaTypes.join(', ')}${
			(this.not || this.mediaTypes.length > 0) && !!this.ranges.length ? ' and ' : ''
		}${this.ranges
			.map(
				(range) =>
					`(${range.before ? `${range.before.value} ${range.before.operator} ` : ''}${range.type}${
						range.after ? ` ${range.after.operator} ${range.after.value}` : ''
					})`
			)
			.join(' and ')}${
			(this.not || this.mediaTypes.length > 0) && !!this.rules.length ? ' and ' : ''
		}${this.rules
			.map((rule) => (rule.value ? `(${rule.name}: ${rule.value})` : `(${rule.name})`))
			.join(' and ')}`;
	}

	/**
	 * Returns "true" if the item matches.
	 */
	public matches(): boolean {
		return this.not ? !this.matchesAll() : this.matchesAll();
	}

	/**
	 * Returns "true" if all matches.
	 *
	 * @returns "true" if all matches.
	 */
	private matchesAll(): boolean {
		if (!!this.mediaTypes.length) {
			let isMediaTypeMatch = false;
			for (const mediaType of this.mediaTypes) {
				if (this.matchesMediaType(mediaType)) {
					isMediaTypeMatch = true;
					break;
				}
			}

			if (!isMediaTypeMatch) {
				return false;
			}
		}

		for (const rule of this.rules) {
			if (!this.matchesRule(rule)) {
				return false;
			}
		}

		for (const range of this.ranges) {
			if (!this.matchesRange(range)) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Returns "true" if the mediaType matches.
	 *
	 * @param mediaType Media type.
	 * @returns "true" if the mediaType matches.
	 */
	private matchesMediaType(mediaType: MediaQueryTypeEnum): boolean {
		if (mediaType === MediaQueryTypeEnum.all) {
			return true;
		}
		return mediaType === new WindowBrowserContext(this.window).getSettings()?.device.mediaType;
	}

	/**
	 * Returns "true" if the range matches.
	 *
	 * @param range Range.
	 * @returns "true" if the range matches.
	 */
	private matchesRange(range: IMediaQueryRange): boolean {
		const windowSize = range.type === 'width' ? this.window.innerWidth : this.window.innerHeight;

		if (range.before) {
			const beforeValue = this.toPixels(range.before.value);

			if (beforeValue === null) {
				return false;
			}

			switch (range.before.operator) {
				case '<':
					if (beforeValue >= windowSize) {
						return false;
					}
					break;
				case '<=':
					if (beforeValue > windowSize) {
						return false;
					}
					break;
				case '>':
					if (beforeValue <= windowSize) {
						return false;
					}
					break;
				case '>=':
					if (beforeValue < windowSize) {
						return false;
					}
					break;
			}
		}

		if (range.after) {
			const afterValue = this.toPixels(range.after.value);

			if (afterValue === null) {
				return false;
			}

			switch (range.after.operator) {
				case '<':
					if (windowSize >= afterValue) {
						return false;
					}
					break;
				case '<=':
					if (windowSize > afterValue) {
						return false;
					}
					break;
				case '>':
					if (windowSize <= afterValue) {
						return false;
					}
					break;
				case '>=':
					if (windowSize < afterValue) {
						return false;
					}
					break;
			}
		}

		return true;
	}

	/**
	 * Returns "true" if the rule matches.
	 *
	 * @param rule Rule.
	 * @returns "true" if the rule matches.
	 */
	private matchesRule(rule: IMediaQueryRule): boolean {
		const settings = new WindowBrowserContext(this.window).getSettings();

		if (!settings) {
			return false;
		}

		if (!rule.value) {
			switch (rule.name) {
				case 'min-width':
				case 'max-width':
				case 'min-height':
				case 'max-height':
				case 'width':
				case 'height':
				case 'orientation':
				case 'prefers-color-scheme':
				case 'hover':
				case 'any-hover':
				case 'any-pointer':
				case 'pointer':
				case 'display-mode':
				case 'min-aspect-ratio':
				case 'max-aspect-ratio':
				case 'aspect-ratio':
					return true;
				case 'prefers-reduced-motion':
					return settings.device.prefersReducedMotion === 'reduce';
				case 'forced-colors':
					return settings.device.forcedColors === 'active';
			}
			return false;
		}

		switch (rule.name) {
			case 'min-width':
				const minWidth = this.toPixels(rule.value);
				return minWidth !== null && this.window.innerWidth >= minWidth;
			case 'max-width':
				const maxWidth = this.toPixels(rule.value);
				return maxWidth !== null && this.window.innerWidth <= maxWidth;
			case 'min-height':
				const minHeight = this.toPixels(rule.value);
				return minHeight !== null && this.window.innerHeight >= minHeight;
			case 'max-height':
				const maxHeight = this.toPixels(rule.value);
				return maxHeight !== null && this.window.innerHeight <= maxHeight;
			case 'width':
				const width = this.toPixels(rule.value);
				return width !== null && this.window.innerWidth === width;
			case 'height':
				const height = this.toPixels(rule.value);
				return height !== null && this.window.innerHeight === height;
			case 'orientation':
				return rule.value === 'landscape'
					? this.window.innerWidth > this.window.innerHeight
					: this.window.innerWidth < this.window.innerHeight;
			case 'prefers-color-scheme':
				return rule.value === settings.device.prefersColorScheme;
			case 'prefers-reduced-motion':
				return rule.value === settings.device.prefersReducedMotion;
			case 'forced-colors':
				return (
					(rule.value === 'none' || rule.value === 'active') &&
					rule.value === settings.device.forcedColors
				);
			case 'any-hover':
			case 'hover':
				if (rule.value === 'none') {
					return this.window.navigator.maxTouchPoints > 0;
				}
				if (rule.value === 'hover') {
					return this.window.navigator.maxTouchPoints === 0;
				}
				return false;
			case 'any-pointer':
			case 'pointer':
				if (rule.value === 'none') {
					return false;
				}

				if (rule.value === 'coarse') {
					return this.window.navigator.maxTouchPoints > 0;
				}

				if (rule.value === 'fine') {
					return this.window.navigator.maxTouchPoints === 0;
				}

				return false;
			case 'display-mode':
				return rule.value === 'browser';
			case 'min-aspect-ratio':
			case 'max-aspect-ratio':
			case 'aspect-ratio':
				const aspectRatio = rule.value.split('/');
				const aspectRatioWidth = parseInt(aspectRatio[0], 10);
				const aspectRatioHeight = parseInt(aspectRatio[1], 10);

				if (isNaN(aspectRatioWidth) || isNaN(aspectRatioHeight)) {
					return false;
				}

				switch (rule.name) {
					case 'min-aspect-ratio':
						return (
							aspectRatioWidth / aspectRatioHeight <=
							this.window.innerWidth / this.window.innerHeight
						);
					case 'max-aspect-ratio':
						return (
							aspectRatioWidth / aspectRatioHeight >=
							this.window.innerWidth / this.window.innerHeight
						);
					case 'aspect-ratio':
						return (
							aspectRatioWidth / aspectRatioHeight ===
							this.window.innerWidth / this.window.innerHeight
						);
				}
		}

		return false;
	}

	/**
	 * Convert to pixels.
	 *
	 * @param value Value.
	 * @returns Value in pixels.
	 */
	private toPixels(value: string): number | null {
		if (
			!new WindowBrowserContext(this.window).getSettings()?.disableComputedStyleRendering &&
			value.endsWith('em')
		) {
			this.rootFontSize =
				this.rootFontSize ||
				parseFloat(this.window.getComputedStyle(this.window.document.documentElement).fontSize);
			return CSSMeasurementConverter.toPixels({
				window: this.window,
				value,
				rootFontSize: this.rootFontSize,
				parentFontSize: this.rootFontSize
			});
		}
		return CSSMeasurementConverter.toPixels({
			window: this.window,
			value,
			rootFontSize: 16,
			parentFontSize: 16
		});
	}
}
