// SPDX-License-Identifier: Apache-2.0
import { ConfigKeyFormatter } from '../config-key-formatter.js';
import { LexerInternalNode } from './lexer-internal-node.js';
import { LexerLeafNode } from './lexer-leaf-node.js';
import { KeyName } from '../key-name.js';
import { ConfigKeyError } from '../config-key-error.js';
import { IllegalArgumentError } from '../../../business/errors/illegal-argument-error.js';
import { FlatKeyMapper } from '../../mapper/impl/flat-key-mapper.js';
export class Lexer {
    tokens;
    formatter;
    _roots = new Map();
    flatMapper;
    _rendered = false;
    constructor(tokens, formatter = ConfigKeyFormatter.instance()) {
        this.tokens = tokens;
        this.formatter = formatter;
        if (!this.tokens) {
            throw new ConfigKeyError('tokens must be provided');
        }
        if (!this.formatter) {
            throw new ConfigKeyError('formatter must be provided');
        }
        this.flatMapper = new FlatKeyMapper(this.formatter);
    }
    get rendered() {
        return this._rendered;
    }
    set rendered(rendered) {
        this._rendered = rendered;
    }
    get rootNodes() {
        if (!this.rendered) {
            this.renderTrees();
        }
        return [...this._roots.values()];
    }
    get tree() {
        if (!this.rendered) {
            this.renderTrees();
        }
        return this._roots;
    }
    nodeFor(key) {
        if (!key) {
            throw new IllegalArgumentError('key must not be null or undefined');
        }
        const segments = this.formatter.split(key);
        if (segments.length === 0 || segments[0].trim().length === 0) {
            throw new IllegalArgumentError('key must not be empty');
        }
        let currentNode = this.tree.get(segments[0]);
        if (!currentNode) {
            return null;
        }
        for (let index = 1; index < segments.length; index++) {
            const segment = segments[index];
            if (currentNode.isLeaf()) {
                return null;
            }
            const inode = currentNode;
            const nextNode = inode.children.find((n) => n.name === segment);
            if (!nextNode) {
                return null;
            }
            currentNode = nextNode;
        }
        return currentNode;
    }
    addValue(key, value) {
        if (!key) {
            throw new IllegalArgumentError('key must not be null or undefined');
        }
        const segments = this.formatter.split(this.formatter.normalize(key));
        const rootNode = this._roots.has(segments[0]) ? this._roots.get(segments[0]) : this.rootNodeFor(segments);
        this.processSegments(rootNode, value, segments);
        this.tokens.set(key, value);
    }
    addOrReplaceObject(key, value) {
        if (!key) {
            throw new IllegalArgumentError('key must not be null or undefined');
        }
        const normalizedKey = this.formatter.normalize(key);
        this.addOrReplaceValue(normalizedKey, null);
        if (!value) {
            return;
        }
        const flatMap = this.flatMapper.flatten(value);
        for (const [k, v] of flatMap.entries()) {
            this.addOrReplaceValue(this.formatter.join(normalizedKey, k), v);
        }
    }
    addOrReplaceArray(key, values) {
        if (!key) {
            throw new IllegalArgumentError('key must not be null or undefined');
        }
        const normalizedKey = this.formatter.normalize(key);
        const node = this.nodeFor(normalizedKey);
        if (node && !node.isArray()) {
            if (node.isRoot()) {
                this._roots.delete(node.name);
                this.tokens.delete(node.name);
            }
            else {
                const parent = node.parent;
                parent.remove(node);
                this.tokens.delete(node.path());
            }
        }
        if (!values) {
            return;
        }
        for (const [index, value] of values.entries()) {
            this.addOrReplaceArrayElement(normalizedKey, index, value);
        }
    }
    addOrReplaceArrayElement(normalizedKey, index, value) {
        if (value === null || value === undefined) {
            this.addOrReplaceValue(this.formatter.join(normalizedKey, index.toString()), null);
        }
        const valueType = typeof value;
        if (valueType === 'string' || valueType === 'number' || valueType === 'boolean' || valueType === 'bigint') {
            this.addOrReplaceValue(this.formatter.join(normalizedKey, index.toString()), value.toString());
        }
        else {
            const flatMap = this.flatMapper.flatten(value);
            for (const [k, value_] of flatMap.entries()) {
                this.addOrReplaceValue(this.formatter.join(normalizedKey, index.toString(), k), value_);
            }
        }
    }
    addOrReplaceValue(key, value) {
        if (!key) {
            throw new IllegalArgumentError('key must not be null or undefined');
        }
        const node = this.nodeFor(key);
        if (node) {
            this.replaceValue(node, value);
        }
        else {
            this.addValue(key, value);
        }
    }
    replaceValue(node, value) {
        if (!node.isLeaf()) {
            throw new ConfigKeyError('key must be a leaf node');
        }
        if (node.isRoot()) {
            this._roots.set(node.name, new LexerLeafNode(null, node.name, value, this.formatter));
            this.tokens.set(node.name, value);
        }
        else {
            this.tokens.set(node.path(), value);
            node.parent.replaceValue(node, value);
        }
    }
    /**
     * Parses the token map and returns all the root nodes.
     *
     * @returns {Node[]} The root nodes.
     */
    renderTrees() {
        if (this.tokens.size === 0 || this.rendered) {
            return;
        }
        const keys = [...this.tokens.keys()];
        // Sort the keys so that we can process them in order.
        keys.sort();
        this.processKeys(keys);
        this.rendered = true;
    }
    processKeys(keys) {
        for (const k of keys) {
            const key = this.formatter.normalize(k);
            const segments = this.formatter.split(key);
            const root = this.rootNodeFor(segments);
            if (!root.isLeaf()) {
                this.processSegments(root, this.tokens.get(key), segments);
            }
        }
    }
    rootNodeFor(keyParts) {
        const rootName = keyParts[0];
        if (this._roots.has(rootName)) {
            return this._roots.get(rootName);
        }
        let array = false;
        let root;
        if (keyParts.length >= 2) {
            const nextSegment = keyParts[1];
            if (KeyName.isArraySegment(nextSegment)) {
                array = true;
            }
            root = new LexerInternalNode(null, rootName, [], array, false, this.formatter);
        }
        else {
            root = new LexerLeafNode(null, rootName, this.tokens.get(rootName), this.formatter);
        }
        this._roots.set(rootName, root);
        return root;
    }
    processSegments(root, value, segments) {
        let currentRoot = root;
        for (let index = 1; index < segments.length; index++) {
            const segment = segments[index];
            let node;
            if (KeyName.isArraySegment(segment)) {
                node = this.processArraySegment(currentRoot, segment, value, index, segments);
            }
            else if (index >= segments.length - 1) {
                node = this.processLeafNode(currentRoot, segment, value);
            }
            else {
                node = this.processIntermediateSegment(currentRoot, segment, index, segments);
            }
            if (node.isInternal()) {
                currentRoot = node;
            }
        }
    }
    /**
     * Processes an array segment. This method will create the necessary node to represent the array index.
     *
     * @param root {LexerInternalNode} the root node of this segment.
     * @param value {string} the value of the key.
     * @param segment {string} the segment to process.
     * @param index {number} the index of the segment in the array.
     * @param segments {string[]} the array of segments.
     * @return {Node} the new root node which should be used as the current root or null if no intermediate/leaf node was
     * created.
     * @private
     */
    processArraySegment(root, segment, value, index, segments) {
        let node = root.children.find((n) => n.name === segment);
        if (node) {
            if (node.isLeaf()) {
                throw new ConfigKeyError(`Cannot add a leaf node to another leaf node [ parent = '${root.path()}', child = '${segment}' ]`);
            }
            return node;
        }
        // Case where the array segment points at a value. Eg: LeafNode
        node =
            index >= segments.length - 1
                ? new LexerLeafNode(root, segment, value, this.formatter)
                : new LexerInternalNode(root, segment, [], false, true, this.formatter);
        root.add(node);
        return node;
    }
    processIntermediateSegment(root, segment, index, segments) {
        const existingNode = root.children.find((n) => n.name === segment);
        if (existingNode) {
            if (existingNode.isLeaf()) {
                throw new ConfigKeyError('Cannot add a leaf node to another leaf node');
            }
            return existingNode;
        }
        let node;
        // root.arrVal.0 = string|number (not handled by this case)
        // root.arrVal.0.scalar = string|number (handles this case)
        if (root.isArray()) {
            node = new LexerInternalNode(root, segment, [], false, true, this.formatter);
        }
        if (index < segments.length - 1) {
            const nextSegment = segments[index + 1];
            if (KeyName.isArraySegment(nextSegment)) {
                node = new LexerInternalNode(root, segment, [], true, false, this.formatter);
            }
        }
        if (!node) {
            node = new LexerInternalNode(root, segment, [], false, false, this.formatter);
        }
        root.add(node);
        return node;
    }
    processLeafNode(root, segment, value) {
        if (root.isArray()) {
            throw new ConfigKeyError(`Cannot add a leaf node to an array node [ parent: '${root.path()}', child: '${segment}' ]`);
        }
        if (root.children.some((n) => n.name === segment)) {
            throw new ConfigKeyError(`Cannot add a leaf node to another leaf node [ parent: '${root.path()}', child: '${segment}' ]`);
        }
        const node = new LexerLeafNode(root, segment, value, this.formatter);
        root.add(node);
        return node;
    }
}
//# sourceMappingURL=lexer.js.map