/**
 * ネスト記法のCSSをフラットなCSSに変換する
 * @author ao-system, Inc.
 * @date 2024-11-08
 */
export class CSSFlattener {
	#cssString				= '';		// 元のCSS文字列
	#lines					= [];		// CSS文字列を行ごとに分割した配列
	#selectorStack			= [];		// 現在のセレクタのスタック(各レベルでのセレクタ配列)
	#outputLines			= [];		// 変換後のCSS行を格納する配列
	#blockStack				= [];		// 現在のブロック(セレクタやat-rule)のスタック
	#inCommentBlock			= false;	// コメントブロック内か否かのフラグ
	#inKeyframesBlock		= false;	// @keyframesブロック内か否かのフラグ
	#keyframesBraceCount	= 0;		// @keyframesブロック内の波括弧のカウント
	#includeComments		= true;		// コメントを出力するか否かのフラグ
	#cssFilePath			= '';		// CSSファイルのパス
	#htmlFilePath			= '';		// HTMLファイルのパス
	constructor() {
	}
	// コメントを出力するか否かのフラグを設定する
	setIncludeComments(includeComments) {
		this.#includeComments = includeComments;
	}
	// CSSファイルのパスとHTMLファイルのパスを設定する
	setFilePaths(cssFilePath, htmlFilePath) {
		this.#cssFilePath = cssFilePath;
		this.#htmlFilePath = htmlFilePath;
	}
	// セレクタを処理して、親セレクタとの組み合わせを行い、フルセレクタを生成
	#processSelector(selector) {
		let parentSelectors = this.#selectorStack[this.#selectorStack.length - 1] || [''];
		// カンマ区切りのセレクタを分割
		let selectors = selector.split(',').map(s => s.trim());
		// 親セレクタとの組み合わせを行う
		let combinedSelectors = [];
		for (let parent of parentSelectors) {
			for (let sel of selectors) {
				let fullSelector = '';
				if (sel.startsWith('>')) {
					// 直下の子セレクタの場合
					sel = sel.slice(1).trim();
					fullSelector = parent ? `${parent} > ${sel}` : `> ${sel}`;
				} else if (sel.includes('&')) {
					// 親セレクタの参照を置換
					fullSelector = parent ? sel.replace(/&/g, parent) : sel.replace(/&/g, '');
				} else {
					// 通常の子孫セレクタの場合
					fullSelector = parent ? `${parent} ${sel}` : sel;
				}
				combinedSelectors.push(fullSelector.trim());
			}
		}
		return combinedSelectors;
	}
	// カスタムなメディアクエリの条件を標準の形式に変換
	#convertMediaQuery(condition) {
		// (width < 550px) を (max-width: 549.98px) に変換
		condition = condition.replace(/\(width\s*<\s*(\d+\.?\d*)px\)/g, (match, p1) => {
			let value = parseFloat(p1) - 0.02;
			return `(max-width: ${value}px)`;
		});
		// (width <= 550px) を (max-width: 550px) に変換
		condition = condition.replace(/\(width\s*<=\s*(\d+\.?\d*)px\)/g, (match, p1) => {
			return `(max-width: ${p1}px)`;
		});
		// (width > 550px) を (min-width: 550.02px) に変換
		condition = condition.replace(/\(width\s*>\s*(\d+\.?\d*)px\)/g, (match, p1) => {
			let value = parseFloat(p1) + 0.02;
			return `(min-width: ${value}px)`;
		});
		// (width >= 550px) を (min-width: 550px) に変換
		condition = condition.replace(/\(width\s*>=\s*(\d+\.?\d*)px\)/g, (match, p1) => {
			return `(min-width: ${p1}px)`;
		});
		return condition;
	}
	// @keyframesブロック内の不要なスペースを削除して、1行の文字列に結合
	#minifyKeyframes(lines) {
		return lines.map(line => line.trim()).join('');
	}
	// CSS 内の url() を正しいパスに置換する
	#adjustUrls(cssText) {
		const urlPattern = /url\(\s*(['"]?)(.*?)\1\s*\)/g;				//'
		const cssDir = this.#cssFilePath.substring(0, this.#cssFilePath.lastIndexOf('/'));
		const htmlDir = this.#htmlFilePath.substring(0, this.#htmlFilePath.lastIndexOf('/'));
		return cssText.replace(urlPattern, (match, quote, url) => {
			// 絶対URLの場合はそのまま
			if (/^(data:|http:\/\/|https:\/\/|\/\/)/.test(url)) {
				return `url(${quote}${url}${quote})`;
			}
			// 相対パスを解決
			const resolvedUrl = this.#relativePath(cssDir, htmlDir, url);
			return `url(${quote}${resolvedUrl}${quote})`;
		});
	}
	// 相対パスを計算するヘルパーメソッド
	#relativePath(fromDir, toDir, relativeUrl) {
		// CSSファイル内のURLを絶対パスに変換
		const absoluteUrl = this.#resolvePath(relativeUrl, fromDir + '/');
		// HTMLファイルからの相対パスを計算
		const relativePath = this.#getRelativePath(toDir + '/', absoluteUrl);
		return relativePath;
	}
	// パスを解決するヘルパーメソッド
	#resolvePath(relativePath, basePath) {
		const stack = basePath.split('/');
		const parts = relativePath.split('/');
		stack.pop(); // 現在のファイル名(または空文字)を削除
		for (let i = 0; i < parts.length; i++) {
			if (parts[i] === '.' || parts[i] === '') continue;
			if (parts[i] === '..') {
				if (stack.length > 1) stack.pop();
			} else {
				stack.push(parts[i]);
			}
		}
		return stack.join('/');
	}
	// from から to への相対パスを計算する
	#getRelativePath(from, to) {
		const fromParts = from.split('/').filter(Boolean);
		const toParts = to.split('/').filter(Boolean);
		while (fromParts.length && toParts.length && fromParts[0] === toParts[0]) {
			fromParts.shift();
			toParts.shift();
		}
		const up = fromParts.map(() => '..');
		return up.concat(toParts).join('/');
	}
	// メインの処理関数。入力されたCSSを解析し、フラットなCSSに変換
	flatten(cssString) {
		this.#cssString = this.#adjustUrls(cssString); // URL を調整
		this.#lines = this.#cssString.split('\n');
		this.#selectorStack = [];
		this.#outputLines = [];
		this.#blockStack = [];
		this.#inCommentBlock = false;
		this.#inKeyframesBlock = false;
		this.#keyframesBraceCount = 0;
		let keyframesLines = [];
		let selectorBuffer = '';
		let collectingSelectors = false;
		let i = 0;
		while (i < this.#lines.length) {
			let line = this.#lines[i];
			let trimmedLine = line.trim();
			// コメントブロック内の場合
			if (this.#inCommentBlock) {
				if (this.#includeComments) {
					this.#outputLines.push(line);
				}
				if (trimmedLine.endsWith('*/')) {
					this.#inCommentBlock = false;
				}
				i++;
				continue;
			}
			// コメントブロックの開始
			if (trimmedLine.startsWith('/*')) {
				this.#inCommentBlock = true;
				if (this.#includeComments) {
					this.#outputLines.push(line);
				}
				if (trimmedLine.endsWith('*/')) {
					// 同じ行でコメントが終了
					this.#inCommentBlock = false;
				}
				i++;
				continue;
			}
			// @keyframesブロックの開始
			if (trimmedLine.startsWith('@keyframes')) {
				this.#inKeyframesBlock = true;
				this.#keyframesBraceCount = 0;
				keyframesLines = [];
				if (trimmedLine.endsWith('{')) {
					this.#keyframesBraceCount++;
				}
				keyframesLines.push(trimmedLine);
				i++;
				continue;
			}
			// @keyframesブロック内の処理
			if (this.#inKeyframesBlock) {
				// 行を収集し、波括弧の数をカウント
				if (trimmedLine.includes('{')) {
					this.#keyframesBraceCount += (trimmedLine.match(/{/g) || []).length;
				}
				if (trimmedLine.includes('}')) {
					this.#keyframesBraceCount -= (trimmedLine.match(/}/g) || []).length;
				}
				keyframesLines.push(trimmedLine);
				if (this.#keyframesBraceCount === 0) {
					// ブロックの終了、不要なスペースを削除して出力
					let minifiedKeyframes = this.#minifyKeyframes(keyframesLines);
					this.#outputLines.push(minifiedKeyframes);
					this.#inKeyframesBlock = false;
				}
				i++;
				continue;
			}
			// 空行はスキップ
			if (trimmedLine.length === 0) {
				i++;
				continue;
			}
			// 既にフラット化されたCSSルールを検出
			if (this.#isFlatCSSRule(trimmedLine)) {
				// 複数行に渡る可能性があるので、ブロックの終わりまで読み込む
				let flatCSSLines = [];
				let braceCount = 0;
				do {
					flatCSSLines.push(this.#lines[i]);
					if (this.#lines[i].includes('{')) {
						braceCount += (this.#lines[i].match(/{/g) || []).length;
					}
					if (this.#lines[i].includes('}')) {
						braceCount -= (this.#lines[i].match(/}/g) || []).length;
					}
					i++;
				} while (braceCount > 0 && i < this.#lines.length);
				// そのまま出力
				this.#outputLines.push(flatCSSLines.join('\n'));
				continue;
			}
			// セレクタ行の処理
			if (collectingSelectors) {
				// セレクタリストの続き
				selectorBuffer += ' ' + trimmedLine;
				if (trimmedLine.endsWith('{')) {
					// セレクタリストの終了
					selectorBuffer = selectorBuffer.slice(0, -1).trim(); // '{' を削除
					let combinedSelectors = this.#processSelector(selectorBuffer);
					this.#selectorStack.push(combinedSelectors);
					this.#blockStack.push({ type: 'selector', name: combinedSelectors });
					selectorBuffer = '';
					collectingSelectors = false;
				}
				i++;
				continue;
			}
			if (trimmedLine.endsWith(',')) {
				// セレクタリストの開始(複数行にわたる)
				collectingSelectors = true;
				selectorBuffer = trimmedLine.slice(0, -1).trim() + ',';
				i++;
				continue;
			}
			if (trimmedLine.endsWith('{')) {
				// セレクタリストが一行で完結している場合
				let selector = trimmedLine.slice(0, -1).trim();
				let combinedSelectors = this.#processSelector(selector);
				this.#selectorStack.push(combinedSelectors);
				this.#blockStack.push({ type: 'selector', name: combinedSelectors });
				i++;
				continue;
			}
			// ブロックの終了
			if (trimmedLine === '}') {
				let lastBlock = this.#blockStack.pop();
				if (lastBlock && lastBlock.type === 'selector') {
					this.#selectorStack.pop();
				}
				i++;
				continue;
			}
			// プロパティ行の処理
			if (this.#selectorStack.length > 0) {
				let currentSelectors = this.#selectorStack[this.#selectorStack.length - 1];
				let atRules = this.#blockStack.filter(block => block.type === 'atRule').map(block => block.name);
				let propertyLine = trimmedLine;
				if (atRules.length > 0) {
					// at-rule内のプロパティ
					let atRulePrefix = atRules.join(' { ') + ' { ';
					let closingBraces = ' }'.repeat(atRules.length);
					for (let selector of currentSelectors) {
						this.#outputLines.push(`${atRulePrefix}${selector} { ${propertyLine} }${closingBraces}`);
					}
				} else {
					// 通常のプロパティ
					for (let selector of currentSelectors) {
						this.#outputLines.push(`${selector} { ${propertyLine} }`);
					}
				}
			}
			i++;
		}
	}
	// 既にフラット化されているCSSルールを検出する
	#isFlatCSSRule(line) {
		// 単純な正規表現でフラットなCSSルールを検出
		const flatCSSRulePattern = /^[^\{\}]+\{\s*[^{}\n]+\s*\}$/;
		return flatCSSRulePattern.test(line);
	}
	// 変換後のCSSを取得
	getOutput() {
		return this.#outputLines.join('\n');
	}
}