(function (factory) {
    typeof define === 'function' && define.amd ? define(factory) :
    factory();
})((function () { 'use strict';

    const SyncHub = {
        _id: false,
        _master: false,
        _channel: null,
        _lastSync: null,
        _tasks: new Map(),

        _isTabVisible: document.visibilityState === 'visible',
        _isTabFocused: true,

        init: function () {
            let self = this;

            self._id = Date.now()
                + '-' + Math.random().toString(36).slice(2, 9)
                + '-' + Math.random().toString(36).slice(2, 9)
                + '-' + Math.random().toString(36).slice(2, 9);

            self._channel = new BroadcastChannel('SyncHub');
            self._channel.onmessage = function (event) {
                switch (event.data.message) {
                    case 'I am master!':
                        self._master = event.data.id;
                        self._lastSync = new Date().getTime();
                        break;
                }
            };

            window.addEventListener('focus', () => {
                let prevState = self._isTabFocused;
                self._isTabFocused = true;

                if (prevState !== self._isTabFocused) {
                    self._control();
                }
            });

            window.addEventListener('blur', () => {
                let prevState = self._isTabFocused;
                self._isTabFocused = false;

                if (prevState !== self._isTabFocused) {
                    self._control();
                }
            });

            document.addEventListener('visibilitychange', () => {
                let prevState = self._isTabVisible;
                self._isTabVisible = document.visibilityState === 'visible';

                if (prevState !== self._isTabVisible) {
                    self._control();
                }
            });

            window.addEventListener('storage', (event) => {
                if (event.key.indexOf('SyncHub__') === 0) {
                    let key = event.key.substring(9, event.key.length);
                    let task = self.getTask(key);

                    if (task) {
                        task.update(
                            JsonLazyDataContainer.fromJson(event.newValue),
                            JsonLazyDataContainer.fromJson(event.oldValue),
                        );
                    }
                }
            });

            setInterval(() => {
                if (self.isMaster()) {
                    self._enslave();
                    return;
                }

                if (new Date().getTime() - self._lastSync > 2000) {
                    self._enslave();
                }
            }, 1000);

            self._control();

            return self;
        },

        isMaster: function () {
            return this._master === this._id;
        },

        registerTask: function (name) {
            let task = new SyncHubTask(this, name);
            this._tasks.set(name, task);

            return task;
        },

        getTask: function (name) {
            return this._tasks.get(name);
        },

        fetchState(name) {
            let value = window.localStorage.getItem('SyncHub__' + name);
            if (value === undefined || value === null) {
                return JsonLazyDataContainer.fromValue(null);
            }

            return JsonLazyDataContainer.fromJson(value);
        },

        storeState(name, newState) {
            window.localStorage.setItem('SyncHub__' + name, newState.getJson());
        },

        _control: function () {
            if (this._isTabVisible && this._isTabFocused) {
                this._enslave();
            }
        },

        _enslave: function () {
            this._master = this._id;
            this._lastSync = new Date().getTime();

            this._channel.postMessage({
                message: 'I am master!',
                id: this._id,
            });
        },
    };

    SyncHub.init();

    class SyncHubTask {
        _hub = null;
        _name = null;

        constructor(hub, name) {
            this._hub = hub;
            this._name = name;

            return this;
        }

        onInterval(callback, interval, runImmediately = false) {
            let self = this;

            let worker = async () => {
                if (self._hub.isMaster()) {
                    this.update(
                        await callback(self)
                    );
                }
            };

            setInterval(worker, interval);

            if (runImmediately) {
                worker();
            }

            return this;
        }

        onUpdate(callback) {
            this._updateCallback = callback;

            return this;
        }

        update(newState, prevState) {
            if (newState === undefined) {
                throw Error('It is necessary to specify the state value!');
            }

            if (! (newState instanceof JsonLazyDataContainer)) {
                newState = JsonLazyDataContainer.fromValue(newState);
            }

            let isInvoker = prevState === undefined;
            if (isInvoker) {
                prevState = this._hub.fetchState(this._name);
            }

            let hasChanged = newState.getJson() !== prevState.getJson();
            if (hasChanged) {
                if (isInvoker) {
                    this._hub.storeState(this._name, newState);
                }

                if (this._updateCallback) {
                    this._updateCallback(newState.getValue(), prevState.getValue());
                }
            }
        }

        getState() {
            return this._hub.fetchState(this._name).getValue();
        }
    }

    class JsonLazyDataContainer {
        _json = undefined;
        _value = undefined;

        static fromJson(json) {
            let self = new this();
            self._json = json;

            return self;
        }

        static fromValue(value) {
            let self = new this();
            self._value = value;

            return self;
        }

        getJson() {
            if (this._json === undefined) {
                this._json = JSON.stringify(this._value);
            }

            return this._json;
        }

        getValue() {
            if (this._value === undefined) {
                this._value = JSON.parse(this._json);
            }

            return this._value;
        }
    }

    if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
        module.exports = SyncHub;
    } else {
        window.SyncHub = SyncHub;
    }

}));
