#!/usr/bin/env python

#Mercurial extension to robustly integrate prompts with other processes
#Copyright (C) 2010-2011 Willem Verstraeten
#
#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.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

import socket
import struct
import urllib2
from mercurial import  ui, util
from mercurial.i18n import _

try:
    from mercurial.url import passwordmgr
except:
    from mercurial.httprepo import passwordmgr

def sendInt( client, number):
    length = struct.pack('>L', number)
    client.sendall( length )

def send( client, data ):
    if data is None:
        sendInt(client, 0)
    else:
        # we need to send data length and data together because it may produce read problems, see org.zmlx.hg4idea.execution.SocketServer
        client.sendall(struct.pack('>L', len(data)) + data)
    
def receiveIntWithMessage(client, message):
    requiredLength = struct.calcsize('>L')
    buffer = ''
    while len(buffer)<requiredLength:
        chunk = client.recv(requiredLength-len(buffer))
        if chunk == '':
            raise util.Abort( message )
        buffer = buffer + chunk
        
    # struct.unpack always returns a tuple, even if that tuple only contains a single
    # item. The trailing , is to destructure the tuple into its first element.
    intToReturn, = struct.unpack('>L', buffer)   
      
    return intToReturn
    
    
def receiveInt(client):
    return receiveIntWithMessage(client, "could not get information from server")

def receive( client ):
    receiveWithMessage(client, "could not get information from server")
    
def receiveWithMessage( client, message ):
    length = receiveIntWithMessage(client, message)
    buffer = ''
    while len(buffer) < length :
        chunk = client.recv(length - len(buffer))
        if chunk == '':
            raise util.Abort( message)
        buffer = buffer+chunk
        
    return buffer

# decorator to cleanly monkey patch methods in mercurial
def monkeypatch_method(cls):
    def decorator(func):
        setattr(cls, func.__name__, func)
        return func
    return decorator

def sendchoicestoidea(ui, msg, choices, default):
    port = int(ui.config( 'hg4ideaprompt', 'port', None, True))

    if not port:
        raise util.Abort("No port was specified")
    if (type(choices) is int) and (type(msg) is str):
        # since Mercurial 2.7 the promptchoice method doesn't accept 'choices' as parameter, so we need to parse them from msg
        # see ui.py -> promptchoice(self, prompt, default=0)
        parts = msg.split('$$')
        msg = parts[0].rstrip(' ')
        choices = [p.strip(' ') for p in parts[1:]]

    numOfChoices = len(choices)
    if not numOfChoices:
        return default

    client = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
    
    try:
        client.connect( ('127.0.0.1', port) )

        send( client, msg )
        sendInt( client, numOfChoices )
        for choice in choices:
            send( client, choice )
        sendInt( client, default )
    
        answer = receiveInt( client )
        if answer == -1:
            raise util.Abort("User cancelled")
        else:      
            return answer
    except:
        raise

# determine which method to monkey patch : 
# in Mercurial 1.4 the prompt method was renamed to promptchoice
if getattr(ui.ui, 'promptchoice', None):
    @monkeypatch_method(ui.ui)
    def promptchoice(self, msg, choices=None, default=0):
        return sendchoicestoidea(self, msg, choices, default)
else:
    @monkeypatch_method(ui.ui)
    def prompt(self, msg, choices=None, default="y"):
        resps = [s[s.index('&')+1].lower() for s in choices]
        defaultIndex = resps.index( default )
        responseIndex = sendchoicestoidea( self, msg, choices, defaultIndex)
        return resps[responseIndex]

original_warn = ui.ui.warn
@monkeypatch_method(ui.ui)
def warn(self, *msg):
    original_warn(self, *msg)
    hg4ideaWarnConfig = self.config('hg4ideawarn', 'port', None, True)
    if hg4ideaWarnConfig is None:
        return
    port = int(hg4ideaWarnConfig)
  
    if not port:
        raise util.Abort("No port was specified")

    self.debug( "hg4idea prompt server waiting on port %s" % port )

    client = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
    
    self.debug( "connecting ..." )
    client.connect( ('127.0.0.1', port) )
    self.debug( "connected, sending data ..." )
    
    sendInt( client, len(msg) )
    for message in msg:
        send( client, message )


def retrieve_pass_from_server(ui, uri,path, proposed_user):
    port = int(ui.config('hg4ideapass', 'port', None, True))
    if port is None:
        raise util.Abort("No port was specified")
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ui.debug("connecting ...")
    client.connect(('127.0.0.1', port))
    ui.debug("connected, sending data ...")
    
    send(client, "getpass")
    send(client, uri)
    send(client, path)
    send(client, proposed_user)
    user = receiveWithMessage(client, "http authorization required")
    password = receiveWithMessage(client, "http authorization required")
    return user, password


original_retrievepass=passwordmgr.find_user_password
@monkeypatch_method(passwordmgr)
def find_user_password(self, realm, authuri):
    try:
        return original_retrievepass(self, realm, authuri)
    except util.Abort:

        # In mercurial 1.8 the readauthtoken method was replaced with
        # the readauthforuri method, which has different semantics
        if getattr(self, 'readauthtoken', None):
            def read_hgrc_authtoken(ui, authuri):
                return self.readauthtoken(authuri)
        else:
            def read_hgrc_authtoken(ui, authuri):
                try:
                    # since hg 1.8
                    from mercurial.url import readauthforuri
                except ImportError:
                    # hg 1.9: readauthforuri moved to httpconnection
                    from mercurial.httpconnection import readauthforuri
                from inspect import getargspec
                args, _, _, _ = getargspec(readauthforuri)
                if len(args) == 2:
                    res = readauthforuri(self.ui, authuri)
                else:
                    # since hg 1.9.2 readauthforuri accepts 3 required arguments instead of 2
                    res = readauthforuri(self.ui, authuri, "")
                if res:
                    group, auth = res
                    return auth
                else:
                    return None

        # After mercurial 3.8.3 urllib2.HTTPPasswordmgrwithdefaultrealm.find_user_password etc were changed to appropriate methods
        # in util.urlreq module with slightly different semantics
        newMerc = False if isinstance(self, urllib2.HTTPPasswordMgrWithDefaultRealm) else True
        if newMerc:
            user, password = util.urlreq.httppasswordmgrwithdefaultrealm().find_user_password(realm, authuri)
        else:
            user, password = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(self, realm, authuri)
        if user is None:
            auth = read_hgrc_authtoken(self.ui, authuri)
            if auth:
                user = auth.get("username")

        pmWithRealm = util.urlreq.httppasswordmgrwithdefaultrealm() if newMerc else self
        reduced_uri, path = pmWithRealm.reduce_uri(authuri, False)
        retrievedPass = retrieve_pass_from_server(self.ui, reduced_uri, path, user)
        if retrievedPass is None:
            raise util.Abort(_('http authorization required'))
        user, passwd = retrievedPass
        pmWithRealm.add_password(realm, authuri, user, passwd)
        return retrievedPass