import React from 'react';
import {withContextConsumer} from "@formanta/react";
import {ID} from "@formanta/js";
import Pushing from './Pushing';

const PusherContext = React.createContext({});

class PusherProvider extends React.Component {
    constructor(props) {
        super(props);

        /**
         *
         * @type {{pushingIds: Array, pusherStats: {all: number, doing: number, success: number, failed: number}, addPushing: (function(Pushing): *), unbindPushing: PusherProvider.unbindPushing, pushings: Object.<Pushing>}}
         */
        this.state = {
            pushings: {},
            pushingIds: [],
            pusherStats: {
                all: 0,
                doing: 0,
                success: 0,
                failed: 0,
            },
            addPushing: this.addPushing,
            execPushing: this.execPushing,
            unbindPushing: this.unbindPushing,
        };

    }

    getStat(type) {
        if(!this.state.pusherStats.hasOwnProperty(type)) {
            console.error('Pusher: tried to get stats for undefined type `' + type + '`');
            return 0;
        }
        return this.state.pusherStats[type];
    }

    updateStats(type, val) {
        const pusherStats = {...this.state.pusherStats};
        if(!pusherStats.hasOwnProperty(type)) {
            console.error('Pusher: tried to update stats for undefined type `' + type + '`');
            return;
        }
        pusherStats[type] = val;
        this.setState({pusherStats});
    }

    /**
     * @param {Pushing} pushing
     * @return {*}
     */
    addPushing = (pushing) => {
        const pushings = {...this.state.pushings};

        const id = ID();
        pushings[id] = pushing;
        this.updateStats('all', this.getStat('all') + 1);
        this.setState({pushings});

        return {
            id,
            promise: this.execPushing(pushing, id)
        };
    };

    unbindPushing = (id) => {
        if(this.state.pushings.hasOwnProperty(id)) {
            const pushings = {...this.state.pushings};
            pushings[id].boundToDispatcher = false;
            this.setState({pushings});
        }
    };

    execPushing = (pushing, id) => {
        /**
         * init with dummy handle
         * @param res
         * @param rej
         */
        let handle = (res, rej) => {
            rej()
        };
        const pushings = {...this.state.pushings};
        if(!pushings.hasOwnProperty(id)) {
            pushings[id] = pushing;
        }
        if(pushings[id] && pushings[id].request) {
            handle = pushings[id].request;
        }

        pushings[id].state = 'begin';
        this.updateStats('doing', this.getStat('doing') + 1);

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

            const res = (data) => {
                pushings[id].state = 'success';
                this.updateStats('success', this.getStat('success') + 1);
                this.updateStats('doing', this.getStat('doing') - 1);

                if(pushings[id].boundToDispatcher) {
                    resolve(data);
                    return;
                }
                reject({cancelled: true});
            };

            const rej = (err) => {
                pushings[id].state = 'failed';
                this.updateStats('failed', this.getStat('failed') + 1);
                this.updateStats('doing', this.getStat('doing') - 1);

                if(pushings[id].boundToDispatcher) {
                    reject(err);
                    return;
                }
                reject({cancelled: true});
            };

            handle(res, rej);
        });
    };

    render() {
        return (
            <PusherContext.Provider value={this.state}>
                {this.props.children}
            </PusherContext.Provider>
        );
    }
}

const withPusher = (WrappedComponent) =>
    withContextConsumer(PusherContext.Consumer)(
        class PusherHOC extends React.Component {
            /**
             * all id's of pushings dispatched from wrapped component
             * @type {Array}
             */
            dispatchedPushings = [];

            constructor(props) {
                super(props);
                this.execPushing = this.execPushing.bind(this);
            }

            componentWillUnmount() {
                const {unbindPushing} = this.props;

                this.dispatchedPushings.forEach(id => {
                    unbindPushing(id);
                });
                this.dispatchedPushings = [];
            }

            /**
             * This wraps addPushing from the Provider to be able to have cancelable Promises/unbinding triggered pushed from bubbling down
             *
             * @param {Pushing} pushing
             * @returns {Promise<any>}
             */
            execPushing(pushing) {
                const {addPushing} = this.props;

                let {id, promise} = addPushing(pushing);
                this.dispatchedPushings.push(id);

                return promise;
            }

            render() {
                return (
                    <WrappedComponent {...this.props} execPushing={this.execPushing}/>
                );
            }
        }
    );

export {PusherProvider, withPusher, Pushing};