/**
 * Copyright schukai GmbH and contributors 2022. All Rights Reserved.
 * Node module: @schukai/monster
 * This file is licensed under the AGPLv3 License.
 * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
 */

import {internalSymbol, instanceSymbol} from "../../constants.mjs";
import {isString, isObject} from "../../types/is.mjs";
import {WebConnect} from "../../net/webconnect.mjs";
import {Message} from "../../net/webconnect/message.mjs";
import {Datasource} from "../datasource.mjs";
import {Pathfinder} from "../pathfinder.mjs";
import {Pipe} from "../pipe.mjs";

export {WebSocketDatasource}


/**
 * @private
 * @type {Symbol}
 *
 * hint: this name is used in the tests. if you want to change it, please change it in the tests as well.
 */
const webConnectSymbol = Symbol("connection");

/**
 *
 * @param self
 * @param obj
 * @returns {*}
 */
function doTransform(type, obj) {
    const self = this;
    let transformation = self.getOption(type + '.mapping.transformer');
    if (transformation !== undefined) {
        const pipe = new Pipe(transformation);
        const callbacks = self.getOption(type + '.mapping.callbacks')

        if (isObject(callbacks)) {
            for (const key in callbacks) {
                if (callbacks.hasOwnProperty(key) && typeof callbacks[key] === 'function') {
                    pipe.setCallback(key, callbacks[key]);
                }
            }
        }

        obj = pipe.run(obj);
    }

    return obj;
}

/**
 * The RestAPI is a class that enables a REST API server.
 *
 * @externalExample ../../../example/data/storage/restapi.mjs
 * @license AGPLv3
 * @since 3.1.0
 * @copyright schukai GmbH
 * @memberOf Monster.Data.Datasource
 * @summary The LocalStorage class encapsulates the access to data objects.
 */
class WebSocketDatasource extends Datasource {

    /**
     *
     * @param {Object} [options] options contains definitions for the datasource.
     */
    constructor(options) {
        super();
        
        const self = this;

        if (isString(options)) {
            options = {url: options};
        }

        if (!isObject(options)) options = {};
        this.setOptions(options);
        this[webConnectSymbol] = new WebConnect({
            url: self.getOption('url'),
            connection: {
                timeout: self.getOption('connection.timeout'),
                reconnect: {
                    timeout: self.getOption('connection.reconnect.timeout'),
                    attempts: self.getOption('connection.reconnect.attempts'),
                    enabled: self.getOption('connection.reconnect.enabled')
                }
            }
        });
    }

    /**
     *
     * @returns {Promise}
     */
    connect() {
        return this[webConnectSymbol].connect();
    }

    /**
     * @returns {boolean}
     */
    isConnected() {
        return this[webConnectSymbol].isConnected();
    }

    /**
     * This method is called by the `instanceof` operator.
     * @returns {symbol}
     */
    static get [instanceSymbol]() {
        return Symbol.for("@schukai/monster/data/datasource/websocket");
    }

    /**
     * @property {string} url=undefined Defines the resource that you wish to fetch.
     * @property {Object} connection
     * @property {Object} connection.timeout=5000 Defines the timeout for the connection.
     * @property {Number} connection.reconnect.timeout The timeout in milliseconds for the reconnect.
     * @property {Number} connection.reconnect.attempts The maximum number of reconnects.
     * @property {Bool} connection.reconnect.enabled If the reconnect is enabled.
     * @property {Object} write={} Options
     * @property {Object} write.mapping the mapping is applied before writing.
     * @property {String} write.mapping.transformer Transformer to select the appropriate entries
     * @property {Monster.Data.Datasource~exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing.
     * @property {Object} write.sheathing
     * @property {Object} write.sheathing.object Object to be wrapped
     * @property {string} write.sheathing.path Path to the data
     * @property {Object} read={} Options
     * @property {String} read.path Path to data
     * @property {Object} read.mapping the mapping is applied after reading.
     * @property {String} read.mapping.transformer Transformer to select the appropriate entries
     * @property {Monster.Data.Datasource~exampleCallback[]} read.mapping.callback with the help of the callback, the structures can be adjusted after reading.
     */
    get defaults() {
        return Object.assign({}, super.defaults, {
            url: undefined,
            write: {
                mapping: {
                    transformer: undefined,
                    callbacks: {}
                },
                sheathing: {
                    object: undefined,
                    path: undefined,
                },
            },
            read: {
                mapping: {
                    transformer: undefined,
                    callbacks: {}
                },
                path: undefined,
            },
            connection: {
                timeout: 5000,
                reconnect: {
                    timeout: 1000,
                    attempts: 1,
                    enabled: false,
                }
            }
        });
    }

    /**
     * This method closes the connection.
     *
     * @returns {Promise}
     */
    close() {
        return this[webConnectSymbol].close();
    }

    /**
     * @return {Promise}
     */
    read() {
        const self = this;

        return new Promise((resolve, reject) => {

            while (this[webConnectSymbol].dataReceived() === true) {
                let obj = this[webConnectSymbol].poll();
                if (!isObject(obj)) {
                    reject(new Error('The received data is not an object.'));
                    return;
                }

                if (!(obj instanceof Message)) {
                    reject(new Error('The received data is not a Message.'));
                    return;
                }

                obj = obj.getData();

                obj = self.transformServerPayload.call(self, obj);
                self.set( obj);
            }

            resolve(self.get());

        })

    };

    /**
     * This prepares the data that comes from the server.
     * Should not be called directly.
     *
     * @private
     * @param {Object} payload
     * @returns {Object}
     */
    transformServerPayload(payload) {
        const self = this;
        payload = doTransform.call(self, 'read', payload);

        const dataPath = self.getOption('read.path');
        if (dataPath) {
            payload = (new Pathfinder(payload)).getVia(dataPath);
        }

        return payload;
    }

    /**
     * This prepares the data for writing and should not be called directly.
     *
     * @private
     * @param {Object} payload
     * @returns {Object}
     */
    prepareServerPayload(payload) {
        const self = this;

        payload = doTransform.call(self, 'write', payload);

        let sheathingObject = self.getOption('write.sheathing.object');
        let sheathingPath = self.getOption('write.sheathing.path');

        if (sheathingObject && sheathingPath) {
            const sub = payload;
            payload = sheathingObject;
            (new Pathfinder(payload)).setVia(sheathingPath, sub);
        }

        return payload;
    }

    /**
     * @return {Promise}
     */
    write() {
        const self = this;
        let obj = self.prepareServerPayload(self.get());
        return self[webConnectSymbol].send(obj)
    }


    /**
     * @return {RestAPI}
     */
    getClone() {
        const self = this;
        return new WebSocketDatasource(self[internalSymbol].getRealSubject()['options']);
    }

}