'''
..
  $Id$
  
  GTK python tools of the clazzes.org project
  http://www.clazzes.org
 
  Created: 21.09.2018
 
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
 
      http://www.apache.org/licenses/LICENSE-2.0
 
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License. 
'''

from gi.repository import Gtk
from clazzes.gtk import worker
from clazzes.http import xmlrpc
from clazzes.gtk.l10n import i18n

import logging
import threading

log = logging.getLogger(__name__)

_the_login_info_lock = threading.Lock()
_the_login_info = None

def getLoginInfo():
    '''
       :return: The currently active login information.
    '''
    with _the_login_info_lock:
        return _the_login_info

def setLoginInfo(login_info: xmlrpc.LoginInfo):
    '''
       :param login_info: The currently active login information to set.
    '''
    global _the_login_info
    
    with _the_login_info_lock:
        _the_login_info = login_info


class UserPasswordDialog(Gtk.Dialog):
    
    def __init__(self,parent):
        Gtk.Dialog.__init__(self, i18n("sign on"), parent, 0,
        (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
             Gtk.STOCK_OK, Gtk.ResponseType.OK))
        
        grid = Gtk.Grid()
        
        irow = 1
        
        self.usernamewidget = Gtk.Entry()
        self.get_username = self.usernamewidget.get_text
        usernamelabel = Gtk.Label(i18n("User Name"))
        usernamelabel.set_halign(Gtk.Align.START)
        grid.attach(usernamelabel,1,irow,1,1)
        self.usernamewidget.set_hexpand(True)
        grid.attach(self.usernamewidget,2,irow,1,1)
        
        irow += 1
        self.passwordwidget = Gtk.Entry()
        self.passwordwidget.set_visibility(False)
        self.get_password = self.passwordwidget.get_text
        passwordlabel = Gtk.Label(i18n("Password"))
        passwordlabel.set_halign(Gtk.Align.START)
        grid.attach(passwordlabel,1,irow,1,1)
        self.passwordwidget.set_hexpand(True)
        grid.attach(self.passwordwidget,2,irow,1,1)
        
        grid.show_all()
        self.get_content_area().add(grid)

class TokenOtpDialog(Gtk.Dialog):
    
    def __init__(self,parent, may_send_sms = False):
        
        if may_send_sms:
            actions = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                       i18n("Send SMS OTP"),Gtk.ResponseType.NO,
             Gtk.STOCK_OK, Gtk.ResponseType.OK)
        else:
            actions = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
             Gtk.STOCK_OK, Gtk.ResponseType.OK)
        
        Gtk.Dialog.__init__(self,i18n("Enter Token OTP"), parent, 0,
                            actions)
        
        grid = Gtk.Grid()
        
        irow = 1
        
        self.otpwidget = Gtk.Entry()
        self.get_otp = self.otpwidget.get_text
        otplabel = Gtk.Label(i18n("Token OTP"))
        otplabel.set_halign(Gtk.Align.START)
        grid.attach(otplabel,1,irow,1,1)
        self.otpwidget.set_hexpand(True)
        grid.attach(self.otpwidget,2,irow,1,1)
    
        grid.show_all()
        self.get_content_area().add(grid)

class SmsOtpDialog(Gtk.Dialog):
    
    def __init__(self,parent):
        
        Gtk.Dialog.__init__(self,i18n("Enter SMS OTP"), parent, 0,
                            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
             Gtk.STOCK_OK, Gtk.ResponseType.OK))
        
        grid = Gtk.Grid()
        
        irow = 1
        
        self.otpwidget = Gtk.Entry()
        self.get_otp = self.otpwidget.get_text
        otplabel = Gtk.Label(i18n("SMS OTP"))
        otplabel.set_halign(Gtk.Align.START)
        grid.attach(otplabel,1,irow,1,1)
        self.otpwidget.set_hexpand(True)
        grid.attach(self.otpwidget,2,irow,1,1)
    
        grid.show_all()
        self.get_content_area().add(grid)

class LoginRunningDialog(Gtk.Dialog):
    
    def __init__(self,parent,label_text):
        
        Gtk.Dialog.__init__(self, i18n("Login running..."), parent, 0,
                            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL))
        
        lbl = Gtk.Label(label_text)
        lbl.set_halign(Gtk.Align.CENTER)
        lbl.show()
        self.get_content_area().add(lbl)
    
class AuthenticationCancelledException(Exception):
    pass

class AuthenticationFailedException(Exception):
    pass

_login_operations = {}

class LoginOperation:
    
    def __init__(self,baseurl,login_url):
        self.key = (baseurl,login_url)
        self.login_service = xmlrpc.LoginService(baseurl,login_url)
        self.deferred_actions = []
        self._login_running_dlg = None
        
    def _raise_login_exception(self,ex):
        for action in self.deferred_actions:
            action[3](action[1],action[0],ex,action[2])

        self.deferred_actions.clear()
        if  _login_operations.get(self.key) == self:
            del _login_operations[self.key]

    def _running_dlg_cancelled(self,a,b):  # @UnusedVariable
        log.info("User cancelled a running login operation.")
        self._hide_running_dlg()
        self._raise_login_exception(AuthenticationCancelledException())
        
    def _show_running_dlg(self,label_text):
        self._login_running_dlg = LoginRunningDialog(None,label_text)
        self._login_running_dlg.show()
        self._login_running_dlg.connect("response",self._running_dlg_cancelled)
    
    def _hide_running_dlg(self):
        if self._login_running_dlg is not None:
            self._login_running_dlg.hide()
            self._login_running_dlg.destroy()
            self._login_running_dlg = None
            #self._login_running_dlg.response(Gtk.ResponseType.OK)

    def _on_login_exception(self,worker,invocation,ex,onsuccess):  # @UnusedVariable
        self._hide_running_dlg()
        self._raise_login_exception(ex)
        return False

    def _on_login_success(self,worker,invocation,login_info : xmlrpc.LoginInfo):  # @UnusedVariable
        
        self._hide_running_dlg()
        
        if len(self.deferred_actions) == 0:
            # User has already cancelled the logon operation.
            return False
        
        if login_info.status == 200:
        
            setLoginInfo(login_info)
        
            for action in self.deferred_actions:
                action[1].invoke(action[0],action[2],action[3])
                
            self.deferred_actions.clear()
            if _login_operations.get(self.key) == self:
                del _login_operations[self.key]
    
        elif login_info.status == xmlrpc.STATUS_EXPECT_TOKEN_OTP:
            dlg = TokenOtpDialog(None,login_info.may_send_sms)
            ret = dlg.run()
            args = (login_info,dlg.get_otp())
            dlg.destroy()
    
            if ret == Gtk.ResponseType.CANCEL or ret == Gtk.ResponseType.DELETE_EVENT:
                self._raise_login_exception(AuthenticationCancelledException())
    
            elif ret == Gtk.ResponseType.NO:
                worker.callMethod(self.login_service.generateSmsOtp,
                                     (login_info,),
                                     onexception=self._on_login_exception,
                                     onsuccess=self._on_login_success)
                self._show_running_dlg(i18n("Generating SMS OTP..."))
                
            else:
                worker.callMethod(self.login_service.sendTokenOtp,
                                         args,
                                         onexception=self._on_login_exception,
                                         onsuccess=self._on_login_success)
                self._show_running_dlg(i18n("Checking Token OTP..."))

        elif login_info.status == xmlrpc.STATUS_EXPECT_EPHEMERAL_OTP:
            dlg = SmsOtpDialog(None)
            ret = dlg.run()
            args = (login_info,dlg.get_otp())
            dlg.destroy()
    
            if ret == Gtk.ResponseType.CANCEL or ret == Gtk.ResponseType.DELETE_EVENT:
                self._raise_login_exception(AuthenticationCancelledException())
    
            worker.callMethod(self.login_service.sendEphemeralOtp,
                                     args,
                                     onexception=self._on_login_exception,
                                     onsuccess=self._on_login_success)
            self._show_running_dlg(i18n("Checking SMS OTP..."))

        else:
            self._raise_login_exception(AuthenticationFailedException())
    
        return False
    
    def addDeferredAction(self,invocation,worker,onsuccess,onexception):
    
        self.deferred_actions.append((invocation,worker,onsuccess,onexception))
    
    def doLogin(self):
    
        dlg = UserPasswordDialog(None)
        ret = dlg.run()
        args = (dlg.get_username(),dlg.get_password())
        dlg.destroy()
        
        if ret == Gtk.ResponseType.CANCEL or ret == Gtk.ResponseType.DELETE_EVENT:
            self._raise_login_exception(AuthenticationCancelledException())
    
        worker.THE_WORKER.callMethod(self.login_service.performLogin,
                                     args,
                                     onexception=self._on_login_exception,
                                     onsuccess=self._on_login_success)
        self._show_running_dlg(i18n("Checking username and password..."))
    
def relogin_guard(onexception):
    def wrapper(self,worker,invocation,ex,onsuccess):
        if isinstance(ex,xmlrpc.XmlRpcTargetError):
            if ex.code == -32099 and ex.data is not None:
                login_url = ex.data.get("loginUrl")
            else:
                login_url = None
            
            if login_url is not None:
                log.info("Call to [%s] requires a login from URL [%s]"%(invocation,login_url))

                key = (ex.baseurl,login_url)
                op = _login_operations.get(key)
    
                if op is None:
                    op = LoginOperation(ex.baseurl,login_url)
                    _login_operations[key] = op
                    op.addDeferredAction(invocation,worker,onsuccess,onexception.__get__(self,type(self)))
                    op.doLogin()
                else:
                    op.addDeferredAction(invocation,worker,onsuccess,onexception.__get__(self,type(self)))
            
                return False

        return onexception(self,worker,invocation,ex,onsuccess)
        
    return wrapper