_import {stdout} from 'node:process';
import {IO} from './IO.js';
import {Rule} from './Rule.js';

/**
 * A collection of one or more modification rules.
 *
 * @example // Create a new RuleSet
 * let rules = new RuleSet('My Rules');
 *
 */
export class RuleSet {
	/**
	 * Instantiates RuleSet from a JSON string or object.
	 * @param {string|object} data - JSON string or object
	 * @return {RuleSet} New instance
	 */
	static fromJSON(data) {
		return new this().loadJSON(data);
	}
	/**
	 * Instantiates RuleSet from a JSON file.
	 * Ruleset files are normally located at `~/.config/karabiner/complex_modifications/*.json`.
	 * @param {string} file - JSON file path
	 * @param {object} [opts] - IO options
	 * @return {RuleSet} New instance
	 */
	static fromFile(file, opts = {}) {
		return new this().setIO(file, opts).load();
	}
	/**
	 * @param {string} title - Title of this ruleset
	 */
	constructor(title) {
		/**
		 * Title of this RuleSet, which is recognized by Karabiner.
		 * @type {string}
		 */
		this.title = title || '';
		/**
		 * Added rules.
		 * @type {Rule[]}
		 */
		this.rules = [];
		/**
		 * IO object for reading/writing this ruleset from/to a file.
		 * @type {IO}
		 */
		this.io;
	}
	/**
	 * Returns a JSON representation of this ruleset.
	 * @param {boolean} [stringify=false] - If `true`, returns a stringified result
	 * @return {object|string} An object like: `{ title: ... , rules: ... }`
	 * @example
	 * let rules = new RuleSet('My Rules');
	 * let obj = rules.toJSON();
	 * console.log( obj.title ); // 'My Rules'
	 */
	toJSON(stringify = false) {
		let r = {
			title: this.title,
			rules: this.rules.map(item => item.toJSON())
		};
		return stringify ? JSON.stringify(r, null, 2) : r;
	}
	/**
	 * Outputs JSON representation of this ruleset to STDOUT.
	 */
	out() {
		stdout.write(this.toJSON(true));
	}
	/**
	 * Setup {@link IO} object for reading/writing this ruleset from/to a file.
	 * Ruleset files are normally located at `~/.config/karabiner/complex_modifications/*.json`.
	 * @param {string} file - Ruleset file path
	 * @param {object} [opts] - IO options
	 * @return {RuleSet} Itself
	 */
	setIO(file, opts = {}) {
		this.io = new IO(file, opts);
		return this;
	}
	/**
	 * Adds an rule to this ruleset.
	 * If the provided argument is a string, a new instance of {@link Rule} will be created with the string as its description.
	 * If the provided argument is an instance of {@link Rule}, simply adds it to the collection.
	 * @param {string|Rule} rule - rule description or an instance of {@link Rule}
	 * @return {Rule} added rule
	 * @example <caption>Adding a new rule with description</caption>
	 * let rule = rules.add('My 1st rule');
	 * @example <caption>Adding a rule instance</caption>
	 * let rule = rules.add(new Rule('My 1st rule'));
	 */
	add(rule) {
		if (!(rule instanceof Rule)) rule = new Rule(rule);
		this.rules.push(rule);
		return rule;
	}
	/**
	 * Loads JSON data.
	 * @param {string|object} data - JSON string or object
	 * @return {RuleSet} Itself
	 */
	loadJSON(data) {
		data = (typeof data == 'string') ? JSON.parse(data) : data;
		this.title = data.title || '';
		if (Array.isArray(data.rules)) { // add rules
			for (let i = 0; i < data.rules.length; i++) this.add(Rule.fromJSON(data.rules[i]));
		}
		return this;
	}
	/**
	 * Loads data from the ruleset file.
	 * @return {RuleSet} Itself
	 */
	load() {
		if (!this.io) throw `io is not set`;
		this.loadJSON(this.io.read());
		return this;
	}
	/**
	 * Saves this ruleset to the given file in JSON format.
	 * @return {RuleSet} Itself
	 */
	save() {
		if (!this.io) throw `io is not set`;
		this.io.write(this.toJSON(true));
		return this;
	}
}