    'use strict';

    const EventEmitter = require('events');
    const puppeteer = require('puppeteer');
    const moduleRaid = require('moduleraid/moduleraid');
    const jsQR = require('jsqr');

    const Util = require('./util/Util');
    const { WhatsWebURL, UserAgent, DefaultOptions, Events, WAState } = require('./util/Constants');
    const { ExposeStore, LoadUtils } = require('./util/Injected');
    const ChatFactory = require('./factories/ChatFactory');
    const ContactFactory = require('./factories/ContactFactory');
    const { ClientInfo, Message, MessageMedia, Location, GroupNotification } = require('./structures');
    /**
     * Starting point for interacting with the WhatsApp Web API
     * @extends {EventEmitter}
     * @fires Client#qr
     * @fires Client#authenticated
     * @fires Client#auth_failure
     * @fires Client#ready
     * @fires Client#message
     * @fires Client#message_ack
     * @fires Client#message_create
     * @fires Client#message_revoke_me
     * @fires Client#message_revoke_everyone
     * @fires Client#group_join
     * @fires Client#group_leave
     * @fires Client#group_update
     * @fires Client#disconnected
     * @fires Client#change_state
     */
    class Client extends EventEmitter {
        constructor(options = {}) {
            super();

            this.options = Util.mergeDefault(DefaultOptions, options);

            this.pupBrowser = null;
            this.pupPage = null;
        }

        /**
         * Sets up events and requirements, kicks off authentication request
         */
        async initialize() {
            const browser = await puppeteer.launch(this.options.puppeteer);
            const page = (await browser.pages())[0];
            page.setUserAgent(UserAgent);

            if (this.options.session) {
                await page.evaluateOnNewDocument(
                    session => {
                        localStorage.clear();
                        localStorage.setItem('WABrowserId', session.WABrowserId);
                        localStorage.setItem('WASecretBundle', session.WASecretBundle);
                        localStorage.setItem('WAToken1', session.WAToken1);
                        localStorage.setItem('WAToken2', session.WAToken2);
                    }, this.options.session);
            }

            await page.goto(WhatsWebURL);

            const KEEP_PHONE_CONNECTED_IMG_SELECTOR = '[data-asset-intro-image="true"]';

            if (this.options.session) {
                // Check if session restore was successfull
                try {
                    await page.waitForSelector(KEEP_PHONE_CONNECTED_IMG_SELECTOR, { timeout: 15000 });
                } catch (err) {
                    if (err.name === 'TimeoutError') {
                        /**
                         * Emitted when there has been an error while trying to restore an existing session
                         * @event Client#auth_failure
                         * @param {string} message
                         */
                        this.emit(Events.AUTHENTICATION_FAILURE, 'Unable to log in. Are the session details valid?');
                        browser.close();

                        return;
                    }

                    throw err;
                }

            } else {
                const getQrCode = async () => {
                    // Check if retry button is present
                    var QR_RETRY_SELECTOR = 'div[data-ref] > span > div';
                    var qrRetry = await page.$(QR_RETRY_SELECTOR);
                    if (qrRetry) {
                        await qrRetry.click();
                    }

                    // Wait for QR Code

                    const QR_CANVAS_SELECTOR = 'canvas';
                    await page.waitForSelector(QR_CANVAS_SELECTOR);
                    const qrImgData = await page.$eval(QR_CANVAS_SELECTOR, canvas => [].slice.call(canvas.getContext('2d').getImageData(0, 0, 264, 264).data));
                    const qr = jsQR(qrImgData, 264, 264).data;
                    /**
                    * Emitted when the QR code is received
                    * @event Client#qr
                    * @param {string} qr QR Code
                    */
                    this.emit(Events.QR_RECEIVED, qr);
                };
                getQrCode();
                let retryInterval = setInterval(getQrCode, 20000); // check for qr code every 20 seconds

                // Wait for code scan
                await page.waitForSelector(KEEP_PHONE_CONNECTED_IMG_SELECTOR, { timeout: 0 });
                clearInterval(retryInterval);

            }

            await page.evaluate(ExposeStore, moduleRaid.toString());

            // Get session tokens
            const localStorage = JSON.parse(await page.evaluate(() => {
                return JSON.stringify(window.localStorage);
            }));

            const session = {
                WABrowserId: localStorage.WABrowserId,
                WASecretBundle: localStorage.WASecretBundle,
                WAToken1: localStorage.WAToken1,
                WAToken2: localStorage.WAToken2
            };

            /**
             * Emitted when authentication is successful
             * @event Client#authenticated
             * @param {object} session Object containing session information. Can be used to restore the session.
             */
            this.emit(Events.AUTHENTICATED, session);

            // Check window.Store Injection
            await page.waitForFunction('window.Store != undefined');

            //Load util functions (serializers, helper functions)
            await page.evaluate(LoadUtils);

            // Expose client info
            this.info = new ClientInfo(this, await page.evaluate(() => {
                return window.Store.Conn.serialize();
            }));

            // Register events
            await page.exposeFunction('onAddMessageEvent', msg => {
                if (!msg.isNewMsg) return;

                if (msg.type === 'gp2') {
                    const notification = new GroupNotification(this, msg);
                    if (msg.subtype === 'add' || msg.subtype === 'invite') {
                        /**
                         * Emitted when a user joins the chat via invite link or is added by an admin.
                         * @event Client#group_join
                         * @param {GroupNotification} notification GroupNotification with more information about the action
                         */
                        this.emit(Events.GROUP_JOIN, notification);
                    } else if (msg.subtype === 'remove' || msg.subtype === 'leave') {
                        /**
                         * Emitted when a user leaves the chat or is removed by an admin.
                         * @event Client#group_leave
                         * @param {GroupNotification} notification GroupNotification with more information about the action
                         */
                        this.emit(Events.GROUP_LEAVE, notification);
                    } else {
                        /**
                         * Emitted when group settings are updated, such as subject, description or picture.
                         * @event Client#group_update
                         * @param {GroupNotification} notification GroupNotification with more information about the action
                         */
                        this.emit(Events.GROUP_UPDATE, notification);
                    }
                    return;
                }

                const message = new Message(this, msg);

                /**
                 * Emitted when a new message is created, which may include the current user's own messages.
                 * @event Client#message_create
                 * @param {Message} message The message that was created
                 */
                this.emit(Events.MESSAGE_CREATE, message);

                if (msg.id.fromMe) return;

                /**
                 * Emitted when a new message is received.
                 * @event Client#message
                 * @param {Message} message The message that was received
                 */
                this.emit(Events.MESSAGE_RECEIVED, message);
            });

            let last_message;

            await page.exposeFunction('onChangeMessageTypeEvent', (msg) => {

                if (msg.type === 'revoked') {
                    const message = new Message(this, msg);
                    let revoked_msg;
                    if (last_message &amp;&amp; msg.id.id === last_message.id.id) {
                        revoked_msg = new Message(this, last_message);
                    }

                    /**
                     * Emitted when a message is deleted for everyone in the chat.
                     * @event Client#message_revoke_everyone
                     * @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data.
                     * @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data.
                     * Note that due to the way this data is captured, it may be possible that this param will be undefined.
                     */
                    this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg);
                }

            });

            await page.exposeFunction('onChangeMessageEvent', (msg) => {

                if (msg.type !== 'revoked') {
                    last_message = msg;
                }

            });

            await page.exposeFunction('onRemoveMessageEvent', (msg) => {

                if (!msg.isNewMsg) return;

                const message = new Message(this, msg);

                /**
                 * Emitted when a message is deleted by the current user.
                 * @event Client#message_revoke_me
                 * @param {Message} message The message that was revoked
                 */
                this.emit(Events.MESSAGE_REVOKED_ME, message);

            });

            await page.exposeFunction('onMessageAckEvent', (msg, ack) => {

                const message = new Message(this, msg);

                /**
                 * Emitted when an ack event occurrs on message type.
                 * @event Client#message_ack
                 * @param {Message} message The message that was affected
                 * @param {MessageAck} ack The new ACK value
                 */
                this.emit(Events.MESSAGE_ACK, message, ack);

            });

            await page.exposeFunction('onAppStateChangedEvent', (_AppState, state) => {

                /**
                 * Emitted when the connection state changes
                 * @event Client#change_state
                 * @param {WAState} state the new connection state
                 */
                this.emit(Events.STATE_CHANGED, state);

                const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT];
                if (!ACCEPTED_STATES.includes(state)) {
                    /**
                     * Emitted when the client has been disconnected
                     * @event Client#disconnected
                     * @param {WAState} reason state that caused the disconnect
                     */
                    this.emit(Events.DISCONNECTED, state);
                    this.destroy();
                }
            });

            await page.evaluate(() => {
                window.Store.Msg.on('add', (msg) => { if(msg.isNewMsg) window.onAddMessageEvent(msg);});
                window.Store.Msg.on('change', window.onChangeMessageEvent);
                window.Store.Msg.on('change:type', window.onChangeMessageTypeEvent);
                window.Store.Msg.on('change:ack', window.onMessageAckEvent);
                window.Store.Msg.on('remove', window.onRemoveMessageEvent);
                window.Store.AppState.on('change:state', window.onAppStateChangedEvent);
            });

            this.pupBrowser = browser;
            this.pupPage = page;

            /**
             * Emitted when the client has initialized and is ready to receive messages.
             * @event Client#ready
             */
            this.emit(Events.READY);
        }

        /**
         * Closes the client
         */
        async destroy() {
            await this.pupBrowser.close();
        }
        /**
         * Mark as seen for the Chat
         *  @param {string} chatId
         *  @returns {Promise&lt;boolean>} result
         *
         */
        async sendSeen(chatId) {
            const result = await this.pupPage.evaluate(async (chatId) => {
                return window.WWebJS.sendSeen(chatId);

            }, chatId);
            return result;
        }
        /**
         * Send a message to a specific chatId
         * @param {string} chatId
         * @param {string|MessageMedia|Location} content
         * @param {object} options
         * @returns {Promise&lt;Message>} Message that was just sent
         */
        async sendMessage(chatId, content, options = {}) {
            let internalOptions = {
                caption: options.caption,
                quotedMessageId: options.quotedMessageId,
                mentionedJidList: Array.isArray(options.mentions) ? options.mentions.map(contact => contact.id._serialized) : []
            };

            const sendSeen = typeof options.sendSeen === 'undefined' ? true : options.sendSeen;

            if (content instanceof MessageMedia) {
                internalOptions.attachment = content;
                content = '';
            } else if (options.media instanceof MessageMedia) {
                internalOptions.attachment = options.media;
                internalOptions.caption = content;
            } else if (content instanceof Location) {
                internalOptions.location = content;
                content = '';
            }

            const newMessage = await this.pupPage.evaluate(async (chatId, message, options, sendSeen) => {
                let chat = window.Store.Chat.get(chatId);
                let msg;
                if (!chat) { // The chat is not available in the previously chatted list

                    let newChatId = await window.WWebJS.getNumberId(chatId);
                    if (newChatId) {
                        //get the topmost chat object and assign the new chatId to it .
                        //This is just a workaround.May cause problem if there are no chats at all. Need to dig in and emulate how whatsapp web does
                        let chat = window.Store.Chat.models[0];
                        if (!chat)
                            throw 'Chat List empty! Need at least one open conversation with any of your contact';

                        let originalChatObjId = chat.id;
                        chat.id = newChatId;

                        msg = await window.WWebJS.sendMessage(chat, message, options);
                        chat.id = originalChatObjId; //replace the chat with its original id
                    }
                }
                else {
                    if(sendSeen) {
                        window.WWebJS.sendSeen(chatId);
                    }

                    msg = await window.WWebJS.sendMessage(chat, message, options, sendSeen);
                }
                return msg.serialize();
            }, chatId, content, internalOptions, sendSeen);

            return new Message(this, newMessage);
        }

        /**
         * Get all current chat instances
         * @returns {Promise&lt;Array&lt;Chat>>}
         */
        async getChats() {
            let chats = await this.pupPage.evaluate(() => {
                return window.WWebJS.getChats();
            });

            return chats.map(chat => ChatFactory.create(this, chat));
        }

        /**
         * Get chat instance by ID
         * @param {string} chatId
         * @returns {Promise&lt;Chat>}
         */
        async getChatById(chatId) {
            let chat = await this.pupPage.evaluate(chatId => {
                return window.WWebJS.getChat(chatId);
            }, chatId);

            return ChatFactory.create(this, chat);
        }

        /**
         * Get all current contact instances
         * @returns {Promise&lt;Array&lt;Contact>>}
         */
        async getContacts() {
            let contacts = await this.pupPage.evaluate(() => {
                return window.WWebJS.getContacts();
            });

            return contacts.map(contact => ContactFactory.create(this, contact));
        }

        /**
         * Get contact instance by ID
         * @param {string} contactId
         * @returns {Promise&lt;Contact>}
         */
        async getContactById(contactId) {
            let contact = await this.pupPage.evaluate(contactId => {
                return window.WWebJS.getContact(contactId);
            }, contactId);

            return ContactFactory.create(this, contact);
        }

        /**
         * Accepts an invitation to join a group
         * @param {string} inviteCode Invitation code
         */
        async acceptInvite(inviteCode) {
            const chatId = await this.pupPage.evaluate(async inviteCode => {
                return await window.Store.Invite.sendJoinGroupViaInvite(inviteCode);
            }, inviteCode);

            return chatId._serialized;
        }

        /**
         * Sets the current user's status message
         * @param {string} status New status message
         */
        async setStatus(status) {
            await this.pupPage.evaluate(async status => {
                return await window.Store.Wap.sendSetStatus(status);
            }, status);
        }

        /**
         * Gets the current connection state for the client
         * @returns {WAState}
         */
        async getState() {
            return await this.pupPage.evaluate(() => {
                return window.Store.AppState.state;
            });
        }

        /**
         * Enables and returns the archive state of the Chat
         * @returns {boolean}
         */
        async archiveChat(chatId) {
            return await this.pupPage.evaluate(async chatId => {
                let chat = await window.Store.Chat.get(chatId);
                await window.Store.Cmd.archiveChat(chat, true);
                return chat.archive;
            }, chatId);
        }

        /**
         * Changes and returns the archive state of the Chat
         * @returns {boolean}
         */
        async unarchiveChat(chatId) {
            return await this.pupPage.evaluate(async chatId => {
                let chat = await window.Store.Chat.get(chatId);
                await window.Store.Cmd.archiveChat(chat, false);
                return chat.archive;
            }, chatId);
        }

        /**
         * Force reset of connection state for the client
        */
        async resetState(){
            await this.pupPage.evaluate(() => {
                window.Store.AppState.phoneWatchdog.shiftTimer.forceRunNow();
            });
        }

    }

    module.exports = Client;