import Builder from './builder.js';
import log from './log.js';
import { getFirstElementChild, getParserError, stripWhitespace, xmlHtmlNode, xmlescape } from './utils.js';

class UnsafeXML extends String {}

/**
 * A Stanza represents a XML element used in XMPP (commonly referred to as stanzas).
 */
export class Stanza extends Builder {
    /** @type {string} */
    #string;
    /** @type {Array<string>} */
    #strings;
    /**
     * @typedef {Array<string|Stanza|Builder>} StanzaValue
     * @type {StanzaValue|Array<StanzaValue>}
     */
    #values;

    /**
     * @param {string[]} strings
     * @param {any[]} values
     */
    constructor(strings, values) {
        super('stanza');
        this.#strings = strings;
        this.#values = values;
    }

    /**
     * A directive which can be used to pass a string of XML as a value to the
     * stx tagged template literal.
     *
     * It's considered "unsafe" because it can pose a security risk if used with
     * untrusted input.
     *
     * @param {string} string
     * @returns {UnsafeXML}
     * @example
     *    const status = '<status>I am busy!</status>';
     *    const pres = stx`
     *       <presence from='juliet@example.com/chamber' id='pres1'>
     *           <show>dnd</show>
     *           ${unsafeXML(status)}
     *       </presence>`;
     *    connection.send(pres);
     */
    static unsafeXML(string) {
        return new UnsafeXML(string);
    }

    /**
     * Turns the passed-in string into an XML Element.
     * @param {string} string
     * @param {boolean} [throwErrorIfInvalidNS]
     * @returns {Element}
     */
    static toElement(string, throwErrorIfInvalidNS) {
        const doc = xmlHtmlNode(string);
        const parserError = getParserError(doc);
        if (parserError) {
            throw new Error(`Parser Error: ${parserError}`);
        }

        const node = stripWhitespace(getFirstElementChild(doc));
        if (
            ['message', 'iq', 'presence'].includes(node.nodeName.toLowerCase()) &&
            node.namespaceURI !== 'jabber:client' &&
            node.namespaceURI !== 'jabber:server'
        ) {
            const err_msg = `Invalid namespaceURI ${node.namespaceURI}`;
            if (throwErrorIfInvalidNS) {
                throw new Error(err_msg);
            } else {
                log.error(err_msg);
            }
        }
        return node;
    }

    buildTree() {
        return Stanza.toElement(this.toString(), true);
    }

    /**
     * @return {string}
     */
    toString() {
        this.#string =
            this.#string ||
            this.#strings
                .reduce((acc, str, idx) => {
                    const value = this.#values.length > idx ? this.#values[idx] : '';
                    return (
                        acc +
                        str +
                        (Array.isArray(value)
                            ? value
                                  .map((v) =>
                                      v instanceof UnsafeXML || v instanceof Builder
                                          ? v
                                          : xmlescape(v.toString())
                                  )
                                  .join('')
                            : value instanceof UnsafeXML || value instanceof Builder
                              ? value
                              : xmlescape((value ?? '').toString()))
                    );
                }, '')
                .trim();

        return this.#string;
    }
}

/**
 * Tagged template literal function which generates {@link Stanza} objects
 *
 * @example
 *      const pres = stx`<presence type="${type}" xmlns="jabber:client"><show>${show}</show></presence>`
 *
 *      connection.send(msg);
 *
 * @example
 *      const msg = stx`<message
 *          from='sender@example.org'
 *          id='hgn27af1'
 *          to='recipient@example.org'
 *          type='chat'>
 *          <body>Hello world</body>
 *      </message>`;
 *
 *      connection.send(msg);
 *
 * @param {string[]} strings
 * @param {...any} values
 * @returns {Stanza}
 */
export function stx(strings, ...values) {
    return new Stanza(strings, values);
}
