# -*- coding: utf-8 -*-
"""
/***************************************************************************
 ThreadingMaster
                                 A QGIS plugin
 A QGIS Plugin with threading
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2018-12-20
        git sha              : $Format:%H$
        copyright            : (C) 2018 by Alessandro Cristofori
        email                : alessandro.cristofori@halliburton.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""
# Use pdb for debugging
from ..gui.gui_messages import *
import pdb
try:
    import tweepy
except ImportError:
    NoTweepyMessageBox()
import time
import datetime
import os.path
import simplejson
# import qgis.utils
from abc import abstractmethod
from sys import platform
from qgis.core import QgsProject, QgsWkbTypes, QgsVectorLayer, \
    QgsCoordinateReferenceSystem, QgsRectangle
# Initialize Qt resources from file resources.py
from ..resources import *
from PyQt5.QtGui import QIcon, QColor, QDoubleValidator, QPixmap
from PyQt5.QtWidgets import QAction, QStyle, QApplication, QFileDialog
from PyQt5.QtCore import QObject, QSettings, QTranslator, qVersion, \
                         QCoreApplication, pyqtSlot, pyqtRemoveInputHook, \
                         pyqtSignal, Qt
from ..authentication.oauth_credentials import OauthCredentials
from ..authentication.credentials_validator import CredentialsValidator
from ..authentication.mod_tweepy import TweetsHandler, GeoStreamListener, \
    PlaceStreamListener, TweetsAuthHandler
from ..layers.tweet_layers import GeoTweetLayer, PlaceTweetLayer
from ..layers.layer_export import ShpLayerExport

# Import the code for the dialog
from ..gui.threading_master_dialog import ThreadingMasterDialog
from ..gui.oauth_dialog import OAuthCredentialsDialog


class Signals(QObject):
    """General pyQtsignal class"""
    stream_off = pyqtSignal()


class ThreadingMaster:
    """QGIS Plugin Implementation."""

    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
        # Save reference to the QGIS interface
        self.iface = iface
        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)
        # initialize locale
        locale = QSettings().value('locale/userLocale')[0:2]
        locale_path = os.path.join(
            self.plugin_dir,
            'i18n',
            'ThreadingMaster_{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)

            if qVersion() > '4.3.3':
                QCoreApplication.installTranslator(self.translator)

        # Create the dialog (after translation) and keep reference
        self.dlg = ThreadingMasterDialog()
        self.oauth_dlg = OAuthCredentialsDialog()
        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&Qweetgis')
        # TODO: We are going to let the user set this up in a future iteration
        self.toolbar = self.iface.addToolBar(u'Qweetgis')
        self.toolbar.setObjectName(u'Qweetgis')
        self.app_name = (u'Qweetgis')
        # get user OS ['darwin', 'linux', 'win32'] and home directory
        self.user_os = platform
        self.home_dir = os.path.expanduser('~')
        self.env = 'PROD'
        self.canvas = self.iface.mapCanvas()
        self.plugin_crs = QgsCoordinateReferenceSystem('EPSG:4326')
        self.plugin_extent = QgsRectangle(-180.00, -90, 180.00, 90)
        self.credentials_validator = CredentialsValidator(app_name=self.app_name)
        # initialise tweepy classes
        self.credentials = None
        self.tweets_handler = None
        self.tweets_auth = None
        self.tweet_layer = None
        self.api_obj = None
        self.oauth_cred = None
        self.limit = None
        self.limit_type = None
        self.search_type = 'keyword'
        # initialise user interactions
        self.setup_main_dlg_navigation()
        self.setup_oauth_dlg_navigation()
        self.setup_layer_box_navigation()
        # initialise signals 
        self.signals = Signals()
        
    # noinspection PyMethodMayBeStatic
    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate('ThreadingMaster', message)

    def add_action(
        self,
        icon_path,
        text,
        callback,
        enabled_flag=True,
        add_to_menu=True,
        add_to_toolbar=True,
        status_tip=None,
        whats_this=None,
        parent=None):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: FunctiQtto be called when the action is triggered.
        :type callback: functioQt

        :param enabled_flag: A Qtg indicating if the action should be enabled
            by default. DefaultQto True.
        :type enabled_flag: booQt

        :param add_to_menu: FlaQtndicating whether the action should also
            be added to the menQtDefaults to True.
        :type add_to_menu: boolQt

        :param add_to_toolbar: Qtg indicating whether the action should also
            be added to the tooQtr. Defaults to True.
        :type add_to_toolbar: bQt

        :param status_tip: OptiQtl text to show in a popup when mouse pointer
            hovers over the actQt.
        :type status_tip: str

        :param parent: Parent wQtet for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """
        pixmap = QPixmap(icon_path)
        sclaed_pixmap = pixmap.scaled(200, 200, Qt.KeepAspectRatio)
        icon = QIcon(sclaed_pixmap)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            self.toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToMenu(
                self.menu,
                action)

        self.actions.append(action)

        return action

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = ':/plugins/threading_master/icon.svg'
        self.add_action(
            icon_path,
            text=self.tr(u'Qweetgis'),
            callback=self.run_login,
            parent=self.iface.mainWindow())
    
    def setup_main_dlg_navigation(self):
        """ 
        Initialise users' interaction with the main dialog window 
        connect slots to buttons, add items to combo boxes, etc.
        """
        # initialise dialog interactions
        self.dlg.stopButton.setIcon(QApplication.style().standardIcon(QStyle.SP_MediaPause)) 
        self.dlg.stopButton.setEnabled(False) 
        self.dlg.saveLayerButton.setVisible(False) 
        self.dlg.saveLayerButton.setEnabled(False) # change it to false when finished debugg ing
        self.dlg.stopButton.clicked.connect(self.emit_stop_from_worker) 
        self.dlg.streamButton.clicked.connect(self.on_get_stream) 
        self.dlg.saveLayerButton.clicked.connect(self.on_save_layer)
        self.dlg.streamButton.setIcon(QApplication.style().standardIcon(QStyle.SP_MediaPlay))
        self.dlg.SearchTypeDd.addItem("Tweet Location", "_geo_tweets")
        self.dlg.SearchTypeDd.addItem("User Place", "_place_tweets")
        self.dlg.limitDd.addItem("First", "absolute") 
        self.dlg.limitDd.addItem("Up To", "dynamic") 
        self.dlg.limitDd.setEnabled(False) 
        self.dlg.limitSb.setEnabled(False) 
        self.dlg.limitSb.valueChanged.connect(self.set_search_limit)
        self.dlg.limitDd.currentIndexChanged.connect(self.set_limit_type)
        self.dlg.limitCheckBox.stateChanged.connect(self.toggle_limits)
        self.dlg.keywordRadioButton.toggled.connect(lambda: self.toggle_search(self.dlg.keywordRadioButton))
        self.dlg.geoRadioButton.toggled.connect(lambda: self.toggle_search(self.dlg.geoRadioButton))
 
    def setup_oauth_dlg_navigation(self): 
        """  
        Initialise users' interaction with the login dialog window 
        connect slots to buttons, add items to combo boxes, etc.
        """
        self.oauth_dlg.rejected.connect(self.reject_authorise)
        self.oauth_dlg.testButton.setEnabled(False)
        # self.oauth_dlg.buttonBox.buttons()[0].setEnabled(False)
        self.oauth_dlg.testButton.clicked.connect(lambda: \
        self.credentials_validator.test_credentials({
            'CONSUMER_KEY': self.oauth_dlg.consKeyLineEdit.text().strip(),
            'CONSUMER_KEY_SECRET': self.oauth_dlg.secretKeyLineEdit.text().strip(),
            'ACCESS_TOKEN': self.oauth_dlg.userTokenLineEdit.text().strip(),
            'ACCESS_TOKEN_SECRET': self.oauth_dlg.secretTokenLineEdit.text().strip()
        }))
        self.oauth_dlg.consKeyLineEdit.textChanged.connect(self.test_empty_auth_fields)
        self.oauth_dlg.secretKeyLineEdit.textChanged.connect(self.test_empty_auth_fields)
        self.oauth_dlg.userTokenLineEdit.textChanged.connect(self.test_empty_auth_fields)
        self.oauth_dlg.secretTokenLineEdit.textChanged.connect(self.test_empty_auth_fields)

    def setup_layer_box_navigation(self):
        """ 
        Initialise the deafault values and behaviours for the QGISLayerExtent 
        group box widget, whenever the exent changes we change the displayed
        coordinates
        """
        self.dlg.extentGroupBox.setOriginalExtent(self.plugin_extent, self.plugin_crs)
        self.dlg.extentGroupBox.setCurrentExtent(self.plugin_extent, self.plugin_crs)
        self.dlg.extentGroupBox.setOutputCrs(self.plugin_crs)
        self.canvas.extentsChanged.connect(self.set_extent_box)	

    def test_empty_auth_fields(self):
        """
        Checks that every single text field in the authorisation dialog is filled 
        and in case is not disables the "test credentials" button, this is a slot 
        that responds to every change in the authorisation credentials dialog 
        text fields 
        """
        consumer_key_not_empty = bool(len(self.oauth_dlg.consKeyLineEdit.text().strip()) > 0)
        consumer_secret_not_empty = bool(len(self.oauth_dlg.secretKeyLineEdit.text().strip()) > 0)
        access_token_not_empty = bool(len(self.oauth_dlg.userTokenLineEdit.text().strip()) > 0)
        access_token_secret_not_empty = bool(len(self.oauth_dlg.secretTokenLineEdit.text().strip()) > 0)
        if consumer_key_not_empty and consumer_secret_not_empty \
        and access_token_not_empty and access_token_secret_not_empty:
            self.oauth_dlg.testButton.setEnabled(True)
        else:
            self.oauth_dlg.testButton.setEnabled(False)

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        for action in self.actions:
            self.iface.removePluginMenu(
                self.tr(u'&Qweetgis'),
                action)
            self.iface.removeToolBarIcon(action)
        # remove the toolbar
        del self.toolbar
        
    def run_login(self):
        """
        This is the application entry point from the toolbar action,
        the first thing that we check is that the user has the twitter 
        credentials in the config file, if any of the credentials are missing
        does show the login dialog otherwise the main plug-in dialog
        """
        self.credentials = self.get_config_credentials()
        if self.credentials:
            not_authorised = [c for c in self.credentials.values() if c == '']
            if not_authorised:
                # show the login window here
                self.oauth_cred = OauthCredentials(self.oauth_dlg, self.authorise_user)
            else:
                self.run_main()
        else:
            # TODO: a nice error messagebox here
            print('access credentials not found')

    def run_main(self):
        """
        This is the main plug-in dialog method, intialises all the variavbles that 
        will stay alive for the working session (api object) and forces the map 
        to EPSG:4326 and full world extent when the plug-in is started

        :returns: The signal saying if the dialog was accepted or rejected
        :rtype: int

        """
        # user has all credentials saved in config
        self.credentials = self.get_config_credentials()
        self.tweets_auth = TweetsAuthHandler(**self.credentials)
        self.api_obj = self.tweets_auth.get_api_obj()
        current_crs = QgsProject.instance().crs()
        current_extent = self.compare_extents(self.canvas.extent())
        if current_crs.authid() != 'EPSG:4326':
            QgsProject.instance().setCrs(self.plugin_crs)
        self.canvas.setExtent(self.plugin_extent)
        self.canvas.refresh()
        # set the first page of the search type widget
        self.dlg.stackedWidget.setCurrentIndex(0)
        self.dlg.show()
        # Run the dialog event loop
        main_loop_result = self.dlg.exec_()
        # when the user clicks on the OK button (not in the plug-in)
        if main_loop_result:
            self.dlg.keywordRadioButton.setChecked(True)
            if current_crs.authid() != 'EPSG:4326':
                QgsProject.instance().setCrs(current_crs)
            pass
        else:
            """user closes the application ("close button")
            reset all the variables as before initialisation"""
            self.reset_main_dialog()
            self.tweets_auth = None
            self.api_obj = None
            self.dlg.streamLineEdit.setText('')
            self.dlg.tweetsCountLabel.setText('')
            self.dlg.SearchTypeDd.setCurrentIndex(0)
            self.dlg.limitCheckBox.setChecked(False)
            self.dlg.limitDd.setCurrentIndex(0)
            self.dlg.limitDd.setEnabled(False)
            self.dlg.limitSb.setValue(1)
            self.dlg.limitSb.setEnabled(False)
            self.dlg.keywordRadioButton.setChecked(True)
            self.dlg.SearchTypeDd.setCurrentIndex(0)
            # self.dlg.stopButton.disconnect() 
            self.tweet_layer = None
            if current_crs.authid() != 'EPSG:4326':
                QgsProject.instance().setCrs(current_crs)
            self.canvas.setExtent(current_extent)
            self.canvas.refresh()
    
    def compare_extents(self, current_extent):
        """
        compare the current user map extent with the default
        plugin extent (-180, -90, 180, 90) and in case they are
        different saves the current one to reset it when the
        user will close the application

        :param current_extent: the current user extent expressed in 
        lat-long coords (XMin, YMin, XMax, YMax)
        :type current_extent: QgsRectangle

        :returns: QgsRectangle the canvas extent before starting the plugin
        :rtype: QgsRectangle
        
        """
        x_min = self.plugin_extent.xMinimum() if \
                current_extent.xMinimum() == self.plugin_extent.xMinimum()\
                else current_extent.xMinimum()
        y_min = self.plugin_extent.yMinimum() if \
                current_extent.yMinimum() == self.plugin_extent.yMinimum()\
                else current_extent.yMinimum()
        x_max = self.plugin_extent.xMaximum() if \
                current_extent.xMaximum() == self.plugin_extent.xMaximum()\
                else current_extent.xMaximum()
        y_max = self.plugin_extent.yMaximum() if \
                current_extent.yMaximum() == self.plugin_extent.yMaximum()\
                else current_extent.yMaximum()
        return QgsRectangle(x_min, y_min, x_max, y_max)

    def get_config_credentials(self):
        """
        Read-in the config credentials from the
        config/config.json file

        :returns credentials: dictionary storing the user's
        config credentials
        :rtype: dict

        """
        try:
            credentials = dict()
            with open(os.path.join(
                os.path.dirname(os.path.dirname(__file__)),
                'config', 'config.json'), 'r') as f:
                config = simplejson.load(f)
                for key, value in config[self.env]['CREDENTIALS'].items():
                    credentials[key] = value
        except IOError:
            ConfigErrorMessageBox(self.app_name)
        finally:
            return credentials

    def set_config_credentials(self, credentials):
        """
        Set the users' credentials in the config/config.json file.

        :param credentials: all the credentials needed to initialise
            the tweepy auth class
        :type credentials: dict

        :returns: value saying if the credentials were correctly
        read from the config file
        :rtype: bool
    
        """
        try:
            with open(os.path.join(
                os.path.dirname(os.path.dirname(__file__)),
                'config', 'config.json'), 'r') as f:
                config = simplejson.load(f)
            for key, value in credentials.items():
                config[self.env]['CREDENTIALS'][key] = value
            with open(os.path.join(
                os.path.dirname(os.path.dirname(__file__)),
                'config', 'config.json'), 'w') as f:
                simplejson.dump(config, f, indent=4 * ' ')
            return True
        except IOError:
            ConfigErrorMessageBox(self.app_name)
            return False
    
    @pyqtSlot(dict)
    def authorise_user(self, credentials=None):
        """
        Slot to authorise user signal coming from OauthCredentials class
        when the user clicks on OK on the authorise user dialog
        validates the submitted credentials and launch the main dialog
        if credentials are valid and saved to config file, if not returns
        
        :param credentials: all the credentials needed to initialise
                the tweepy auth class
        :type credentials: dict

        """
        self.oauth_dlg.getCredentialsButton.clicked.disconnect()

        self.oauth_dlg.accepted.disconnect()
        self.credentials_validator.set_credentials(credentials) 
        self.credentials_validator.set_parent_dialog(self.oauth_dlg)
        valid_consumer = self.credentials_validator.validate_consumer_keys()
        if valid_consumer:
            save_success = self.set_config_credentials(credentials)
            if save_success:
                self.run_main()
            else:
                return
        else:
            self.oauth_cred.clear_text_fields()
            self.oauth_cred = OauthCredentials(self.oauth_dlg, self.authorise_user)
    
    def set_extent_box(self):
        """
        Slot to update the QgsLayerExtent group box widget output 
        extents whenever the map is moved
        """
        self.dlg.extentGroupBox.setCurrentExtent(self.canvas.extent(), self.plugin_crs)
        self.dlg.extentGroupBox.setOutputExtentFromCurrent()
   
    def reject_authorise(self):
        """
        Slot that responds to the cancel button in the login 
        dialog, resetting all the lineEdits in the dialog 
        and disconnecting the signals
        """
        self.oauth_dlg.getCredentialsButton.clicked.disconnect()
        self.oauth_dlg.accepted.disconnect()
        self.oauth_dlg.consKeyLineEdit.setText('')
        self.oauth_dlg.secretKeyLineEdit.setText('')
        self.oauth_dlg.userTokenLineEdit.setText('')
        self.oauth_dlg.secretTokenLineEdit.setText('')
  
    def toggle_limits(self):
        """
        Slot that responds to the change event on the limitCheckBox
        enabling/disabling the controls to set the streaming
        limits
        """
        state = self.dlg.limitCheckBox.isChecked()
        self.dlg.limitDd.setEnabled(state)
        self.dlg.limitSb.setEnabled(state)
        if state is not True:
            self.limit_type = None
            self.limit = None
        else:
            self.limit_type = self.dlg.limitDd.itemData(self.dlg.limitDd.currentIndex())
            self.limit = self.dlg.limitSb.value()
    
    def toggle_search(self, radio_button):
        """
        Slot that responds to the onChecked events on the 
        keywordRadioButton and geoRadioButton enabling/disabling
        the controls to input the parameters for the two types
        of search

        :param radio button: the radio button on which the change
        event occurred
        :type: QRadioButton
        """
        if radio_button.text() == 'Keyword Search':
            if radio_button.isChecked():
                self.search_type = 'keyword'
                self.dlg.stackedWidget.setCurrentIndex(0)
                self.dlg.extentGroupBox.setEnabled(False)
                self.dlg.streamLineEdit.setEnabled(True)
            else:
                self.search_type = 'geo'
                self.dlg.stackedWidget.setCurrentIndex(1)
                self.dlg.extentGroupBox.setEnabled(True)
                self.dlg.streamLineEdit.setEnabled(False)
        if radio_button.text() == 'Geo Search':
            if radio_button.isChecked():
                self.search_type = 'geo'
                self.dlg.stackedWidget.setCurrentIndex(1)
                self.dlg.extentGroupBox.setEnabled(True)
                self.dlg.streamLineEdit.setEnabled(False)
            else:
                self.search_type = 'keyword'
                self.dlg.stackedWidget.setCurrentIndex(0)
                self.dlg.extentGroupBox.setEnabled(False)
                self.dlg.streamLineEdit.setEnabled(True)


    def set_search_limit(self):
        """
        Sets as class property the numerical limit of the tweets to stream
        from the number spin box
        """
        if self.dlg.limitSb.value() == 0:
            self.dlg.streamButton.setEnabled(False)
            self.dlg.limitSb.setValue(1) 
        self.limit = self.dlg.limitSb.value()
    
    def set_limit_type(self):
        """ 
        Sets as class property the type of limit to apply
        to the stream (absolute/dynamic)
        """
        self.limit_type = self.dlg.limitDd.itemData(self.dlg.limitDd.currentIndex())

    def toggle_ok_button(self):
        """
        Slot that responds to the lineEdit change event on the login
        window, setting the OK button enables when the Twitter DEV API
        credentials are not inputted, so that the user cannot proceed
        """
        if self.oauth_dlg.userTokenLineEdit.text() \
        == '' or self.oauth_dlg.secretTokenLineEdit.text() == '':
            self.oauth_dlg.buttonBox.buttons()[0].setEnabled(False)
        else:
            self.oauth_dlg.buttonBox.buttons()[0].setEnabled(True)
    
    def reset_main_dialog(self):
        """
        Slot for the emit stop signal, emitted only from the stream class,
        and resets buttons on the main dialog
        """
        # resets the dialog buttons to non streaming session
        self.dlg.stopButton.setEnabled(False)
        self.dlg.streamButton.setEnabled(True)
        self.dlg.saveLayerButton.setEnabled(True)
        self.dlg.streamingPb.setValue(0)
        if self.limit is None or (self.limit is not None and self.limit_type == 'dynamic'):
            self.dlg.streamingPb.setRange(0, 100)
        self.dlg.streamingPb.setEnabled(False)
        # destroys the layer instance for this session
        self.tweet_layer = None
    
    def emit_stop_from_worker(self):
        """ 
        Slot from the worker(main thread) for the clicked pause button 
        this slot emits a stop signal to pause the streaming on the 
        listener thread and resets the main ui
        """
        self.signals.stream_off.emit()
        # resets the dialog buttons to non streaming session
        self.reset_main_dialog()

    def tweet_to_file(self, message):
        """Slot for the tweet file signal write to text file the tweets"""
        with open(os.path.join(self.home_dir, 'report.txt'), 'a+') as report:
            report.write(str(message))
    
    def tweet_to_layer(self, geo_tweet):
        """
        Slot for the tweet to layer signal, when a new tweet object is 
        received from the streaming thread calls the layer class 
        to display the tweets as points on the layer canvas

        :param geo_tweet: the object containing the tweet and the 
        tweets information coming from the streaming thread
        :type geo_tweet: dict
        """
        try:
            self.tweet_layer.add_tweet_feature(geo_tweet)
            if self.tweet_layer is not None and (self.limit_type != 'dynamic' or self.search_type != 'geo'):
            # if self.tweet_layer is not None and self.limit_type == None:
                self.tweet_layer.highlight_tweet_feature(geo_tweet, self.iface)
        except:
            pass
    
    def on_stream_error(self, status_error):
        """
        Slot for the error signal emitted from the streaming thread

        :param status_error: the error code produced from the twitter API
        :type status_error: int 
        """
        self.reset_main_dialog()
        StreamErrorMessageBox(self.app_name, status_error)
        return False
    
    def on_save_layer(self):
        map_layers_names = [layer[1].name() for layer in QgsProject.instance().mapLayers().items()]
        if self.tweet_layer is not None and self.tweet_layer.name() in map_layers_names:
            file_name = QFileDialog.getSaveFileName(self.dlg, "Save File",
            os.path.join(self.home_dir, "{0}.{1}".format(self.tweet_layer.name(), "gpkg")),
            "Geopackage (*.gpkg);; Shapefile (*.shp)")
            if file_name[1] != '' and file_name[0] != '':
                shp_export = ShpLayerExport(self.tweet_layer, file_name[0])
                export_error = shp_export.export_layer()
                if export_error is None:
                    ExportSuccessMessageBox(self.app_name, self.tweet_layer.name())
                else:
                    ExportFailMessageBox(self.app_name, self.tweet_layer.name(), export_error)
            else:
                return
        else:
            UndefinedMessageBox(self.app_name, "Tweet Layer")

    
    def add_tweet_layer(self, src_type, src_kw):
        """ 
        Add the in-memory tweets layer to the map canvas, if there is a 
        keyword search not limited already on the map who has the same
        search keywords and has not been made permanent in the same session 
        uses that layer to resume the previously paused streaming
        
        :param src_type: The type of search accuracy the user wants to start
        it can either be based on the provided tweer location (Geo search) or on 
        the user's profile place (Place search)
        :type src_type: string

        :param src_kw: The keyword the user wants to use to filter their search
        :type src_kw: string
        """
        map_layers = [layer[1] for layer in QgsProject.instance().mapLayers().items()
        if type(layer[1]) == GeoTweetLayer or type(layer[1]) == PlaceTweetLayer]
        for layer in map_layers:
            if layer.name() == "{0}{1}".format(src_kw, src_type) and \
            layer.providerType() == 'memory' and \
            layer.wkbType() == QgsWkbTypes.Point and \
            layer.limit_type == self.limit_type and layer.limit == self.limit and \
            self.limit_type == None and self.limit == None:
                self.tweet_layer = layer
        if self.tweet_layer is None:
            if src_type == "_geo_tweets":
                self.tweet_layer = GeoTweetLayer("EPSG:4326", src_kw, self.limit, self.limit_type, self.dlg)
            else:
                self.tweet_layer = PlaceTweetLayer("EPSG:4326", src_kw, self.limit, self.limit_type, self.dlg)
            QgsProject.instance().addMapLayer(self.tweet_layer)
        
    def activate_stream(self, src_type, src_kw=None, src_bbox=None):
        """
        Instantiates the tweepy stream listener, this happens after the 
        layer has been loaded on the canvas

        :param src_type: The type of search accuracy the user wants to start
        it can either be based on the provided tweer location (Geo search) or on 
        the user's profile place (Place search)
        :type src_type: string

        :param src_kw: The keyword the user wants to use to filter their search
        :type src_kw: string

        :param src_bbox: In case the user wants to start a BBOX based stream it 
        contains the extent of the desired search area in (xMin, yMin, xMax, yMax)
        expressed in lon/lat
        :type src_bbx: QgsRetangle
        """
        if src_type == "_geo_tweets":
            stream_listener = GeoStreamListener(self.tweet_to_layer,
            self.signals.stream_off,
            self.on_stream_error,
            self.update_progress_bar_value,
            self.tweet_to_file,
            self.reset_main_dialog,
            api=self.api_obj,
            limit=self.limit,
            limit_type=self.limit_type)
        else:
            stream_listener = PlaceStreamListener(
            self.tweet_to_layer,
            self.signals.stream_off,
            self.on_stream_error,
            self.update_progress_bar_value,
            self.tweet_to_file,
            self.reset_main_dialog,
            api=self.api_obj,
            limit=self.limit,
            limit_type=self.limit_type)
        self.enable_progress_bar(self.limit)
        myStream = tweepy.Stream(auth=self.api_obj.auth, listener=stream_listener)
        if self.search_type == 'keyword':
            myStream.filter(track=src_kw, is_async=True)
        if self.search_type == 'geo':
            result = myStream.filter(locations=src_bbox, is_async=True)
    
    def enable_progress_bar(self, limit):
        """
        Enables the progress bar when a streaming 
        session start setting the max value (100)

        :param limit: the limit to number of tweets set
        by the user, if the param is None it means the
        search is not limited in its results number
        :type limit: int
        
        """
        self.dlg.streamingPb.setEnabled(True)
        
        if limit is not None:
            # progress bar with numbers
            self.dlg.streamingPb.setRange(0, 100)
            self.dlg.streamingPb.setValue(0)
            self.dlg.tweetsCountLabel.setText('')
        else:
            # progress bar showing busy indicator
            self.dlg.streamingPb.setRange(0, 0)


    def update_progress_bar_value(self, value=None, tweets_count=0):
        """ 
        Slot for the update progress signal emitted from the
        streaming thread

        :param value: the number of tweets already streamed 
        from when the listener started 
        :type value: int
        """
        self.dlg.streamingPb.setValue(value)
        self.dlg.tweetsCountLabel.setText(str(tweets_count))

    def on_get_stream(self):
        """ 
        Slot for the StreamButton (play button), fires up two
        different functions (ans stream listeners) if the user wants 
        to stream geographically or keyword filtering tweets
        """
        if self.search_type == 'keyword':
            self.get_keyword_search()
        else:
            self.get_geo_search()
    
    def get_keyword_search(self):
        """
        Keywords filter base streaming
        enable/disable the controls for the 
        streaming session and activates the appropiate
        strem listener
        """
        keywords = self.dlg.streamLineEdit.text().split()
        if len(keywords) == 0: 
            EmptyTextMessageBox(self.app_name)
        else:
            self.dlg.streamButton.setEnabled(False)
            self.dlg.stopButton.setEnabled(True)
            if self.dlg.saveLayerButton.isEnabled():
                self.dlg.saveLayerButton.setEnabled(False)
            accuracy = self.dlg.SearchTypeDd.itemData(self.dlg.SearchTypeDd.currentIndex())
            self.add_tweet_layer(accuracy, "_".join(keywords))
            self.activate_stream(accuracy, src_kw=keywords)
    
    def get_geo_search(self):
        """
        Bounding box based streaming 
        enable/disable the controls for the 
        streaming session and activates the appropiate
        strem listener
        """
        BBOX = self.dlg.extentGroupBox.outputExtent()
        intersection = self.plugin_extent.intersect(BBOX)
        if intersection.toString() == 'Empty':
            EmptyIntersectionMessageBox(self.app_name)
        else:
            self.dlg.streamButton.setEnabled(False)
            self.dlg.stopButton.setEnabled(True)
            if self.dlg.saveLayerButton.isEnabled():
                self.dlg.saveLayerButton.setEnabled(False)
            accuracy = self.dlg.SearchTypeDd.itemData(self.dlg.SearchTypeDd.currentIndex())
            self.add_tweet_layer(accuracy, '_'.join(['BBOXsearch']))
            src_bbox = [intersection.xMinimum(), intersection.yMinimum(), 
                        intersection.xMaximum(), intersection.yMaximum()]
            self.activate_stream(accuracy, src_bbox=src_bbox)