# -*- coding: ISO-8859-1 -*-
""" musicalion_export -- Copyright (c) 2010 musicalion.com
>>> Export the current score to musicalion.com|
For more detailed documentation press the "Documentation" button or open the documentaion site:|
http://www.musicalion.com/en/scores/site/info/about-us
<<<

<?xml version="1.0" encoding="ISO-8859-1"?>
<info>
  <lang id="en">
    <title>->Publish score to Musicalion.com</title>
    <descr>
      <p>With this plugin you can publish the score that's currently open on Musicalion.com</p>
      <p>Important! You need to have already saved the score that you want to publish on your hard drive.</p><p></p>
      <p>If you wish to publish the score in other formats (pdf, midi), please save all the versions with the same name but differnet extensions (e.g. FuerElise.cap, FuerElise.pdf, FuerElise.mid) in the same folder as the capella score file. The plugin will find these files and ask you if you wish to publish them as well.</p><p></p>
      <p>If you don't yet have a Musicalion.com account, you will be forwarded to the registration page. You can then create a FREE account using the following discount code:</p><p></p>
      <p>Discount code: CONTRMUS123</p><p></p>
      <p>You can register using this code and choose your username and password. Once your score is published, you will also be able to download scores from Musicalion.com.</p><p></p>
      <p>For more details please visit: http://www.musicalion.com/en/scores/notes/info/contribute</p>
    </descr>
  </lang>
  <lang id="de">
    <title>->Noten bei Musicalion.com veröffentlichen</title>
    <descr>
      <p>Hiermit können Sie die aktuell geöffneten Noten bei Musicalion.com veröffentlichen.</p><p></p>
      <p>Wichtig! Sie müssen die Noten vorher auf Ihrem Computer gesichert haben.</p><p></p>
      <p>Wenn Sie auch andere Formate (pdf, midi) des gleichen Werkes übermitteln wollen, speichern Sie diese unter gleichem Namen, jedoch mit anderer Endung in das gleiche Verzeichnis (z.B. FuerElise.cap, FuerElise.pdf, FuerElise.mid). Dieses Programm wird diese Dateien dann finden und zur Übertragung ebenfalls vorschlagen.</p><p></p>
      <p>Wenn Sie noch kein Benutzerkonto bei Musicalion.com haben, dann werden Sie hier automatisch zur Anmeldung geleitet. Sie können sich dann KOSTENLOS anmelden, indem Sie folgenden Gutscheincode eingeben:</p><p></p>
      <p>Gutscheincode: CONTRMUS123</p><p></p>
      <p>Damit können Sie sich anmelden und Ihre Zugangsdaten bestimmen. Ein Notendownload von Musicalion.com wird dann erst möglich sein, wenn Sie Ihre Noten übermittelt haben.</p><p></p>
      <p>Mehr dazu finden Sie unter: http://www.musicalion.com/de/scores/notes/info/contribute</p>
    </descr>
  </lang>
</info>
"""

import os, sys, time, urllib, urllib2, httplib, socket, webbrowser, optparse

site = 'www.musicalion.com'
documentationUrl = 'http://%s/%s/scores/site/info/capella'
uploadServiceUrl = 'http://%s/%s/scores/notes/music-upload/service?PHPSESSID=%s'
uploadFormUrl = 'http://%s/sp-mup/%s/scores/notes/music-upload/edit?resetpath=1&layout=popup&PHPSESSID=%s'
passReminderUrl = 'https://%s/sp-fp/%s/scores/members/forgotten-password/edit?layout=popup'
registerUserUrl = 'https://%s/%s/configuration/members/member/sign-up1'
loginServiceUrl = 'https://%s/%s/scores/members/login/service'
localeServiceUrl = 'http://%s/en/admin/translations/locale/list-active'
fileTypeServiceUrl = 'http://%s/en/scores/common/file-type/service'

english = ("en", {
    'user'              :   'Username',
    'password'          :   'Password',
    'noScoreOpen'       :   'There\'s no active score! Please open a score - export only works on your currently active score',
    'scoreNotSaved'     :   'The score hasn\'t been saved yet! Please save it first.',
    'error'             :   'Error',
    'confirmationTitle' :   'Confirmation',
    'confirmationMsg'   :   'Is the score "%s" saved and ready to export ?',
    'confirmationHelp'  :   'Please check that the score is finshed and that the most recent changes have been saved! To upload other formats please use Capella\'s export menu and choose file names based on the original score, using a different extension and/or extended name (e.g. for a single extracted voice, or as a pdf or midi file). For example: if the main score is named "test.cap", then the extracted piano score could be named "test_piano.cap", and the musicXML version should be named "test.xml".',
    'confirmationHint'  :   'For help uploading other formats, please press the "Help" button.',
    'fileBoxTitle'      :   'Choose files to upload',
    'loginTitle'        :   'Login',
    'changeLogin'       :   'Show login dialog to change username/password',
    'loginError'        :   'Login failed !',
    'connectionError'   :   'Connection to Musicalion.com failed!',
    'uploadTitle'       :   'Upload Files',
    'uploadMsg'         :   'The following files will be uploaded: %s This may take a couple of minutes (depending on your internet connection\'s speed). Please wait until the upload form appears!',
    'uploadFailedTitle' :   'Upload Failed',
    'uploadFailedMsg'   :   'Uploading the file: "%s" failed! Please check your internet connection. The error received was: %s',
    'statFailedMsg'     :   'Reading the file: "%s" failed! Please check the file system permissions. The error received was: %s',
    'showHelp'          :   'Show help (opens in external browser after pressing OK)',
    'docRedirect'       :   'This page should redirect in %s seconds, if not please click %s here %s.',
    'loginAction'       :   'Please choose an action:',
    'passReminder'      :   'Send a password reminder',
    'registerUser'      :   'Register on Musicalion.com',
    })

deutsch = ("de", {
    'user'              :   'Benutzername',
    'password'          :   'Passwort',
    'noScoreOpen'       :   'Keine Partitur geöffnet !',
    'scoreNotSaved'     :   'Bitte zuerst die Partitur speichern !',
    'error'             :   'Fehler',
    'confirmationTitle' :   'Bestätigung',
    'confirmationMsg'   :   'Ist der Partitur "%s" abgespeichert und soll sie wirklich so exportiert werden ?',
    'confirmationHelp'  :   'Bitte Überprüfen Sie ob die Partitur Ihren Qualitätsmaßstäben genügt und ob die Datei so abgespeichert ist ! Sie können auch eine pdf- oder midi-Datei hier gleich mit übermitteln, sofern Sie unter dem gleichen Namen (mit anderer Endung: .pdf oder .mid) im gleichen Ordner auf Ihrem Computer gespeichert ist. Beispiel: MeineKomposition.cap, MeineKomposition.pdf, MeineKomposition.mid. Dieses Programm erkennt automatisch die Zugehörigkeit.',
    'confirmationHint'  :   'Für weitere Informationen drücken Sie bitte auf "Hilfe"',
    'fileBoxTitle'      :   'Dateiauswahl',
    'loginTitle'        :   'Login',
    'changeLogin'       :   'Benutzername/Passwort ändern',
    'loginError'        :   'Login ist fehlgeschlagen !',
    'connectionError'   :   'Die Verbindung mit Musicalion.com ist fehlgeschlagen !',
    'uploadTitle'       :   'Dateien Hochladen',
    'uploadMsg'         :   'Die folgende Dateien werden hochgeladen: %s Diese Vorgang kann einige Minute dauern (abhängig von der Geschwindigkeit ihrer Internetanschlusses). Bitte warten Sie bis das "Mitmacher-Formular" von Musicalion.com angezeigt wird. Sobald Sie dort dann einen Komponisten ausgewählt haben, können Sie die restlichen Angaben machen !',
    'uploadFailedTitle' :   'Fehler beim Hochladen',
    'uploadFailedMsg'   :   'Beim Hochladen der Datei: "%s" ist ein Fehler eingetreten ! Bitte überprüfen Sie ihre Internetanschluss. Der Server hat den folgenden Fehler gemeldet : %s',
    'statFailedMsg'     :   'Beim Lesen der Datei: "%s" ist ein Fehler eingetreten ! Bitte überprüfen Sie die Leserechte der Datei. Der gemeldete Fehler war : %s',
    'showHelp'          :   'Hilfe aufrufen (bitte OK klicken, dann wird\'s im Browser angezeigt)',
    'docRedirect'       :   'Diese Seite wird in %s Sekunden umgeleitet, andernfalls klicken Sie bitte %s hier %s.',
    'loginAction'       :   'Wählen Sie bitte ein Befehl:',
    'passReminder'      :   'Passwort vergessen',
    'registerUser'      :   'Es gibt noch kein Passwort: Bei Musicalion.com neu registrieren',
    })

try:
    exec('from %s import translations' % ( translationModule() ))
    translations.append(english)
    translations.append(deutsch)
except:
    translations = [english, deutsch]
setLanguages(translations)

def translate(language, message):
    # This was adapted from cap-obj.py
    # suche nach voller Übereinstimmung, z.B. "en-GB"
    for loc in translations:
        if (loc[0] == language):
            return loc[1].get(message, "???")
    # suche nur nach Sprache, z.B. "en"
    for loc in translations:
        if (loc[0][:2] == language[:2]):
            return loc[1].get(message, "???")
    # erste Sprache nehmen
    return translations[0][1].get(message, "???")

class Settings:
    def __init__(self):
        self.user = ''   
        self.password = ''
        self.firstUsage = True
        self.registered = False

def initSettings():
    global settings, options, opt
    settings = Settings()
    options = ScriptOptions() 
    opt = options.get()
    settings.user       = opt.get('musicalion_export_user', str(settings.user))   
    settings.password   = opt.get('musicalion_export_password', str(settings.password))
    settings.firstUsage = eval(opt.get('musicalion_export_first_usage', str(settings.firstUsage)))
    settings.registered = eval(opt.get('musicalion_export_registered', str(settings.registered)))

def setOptions():
    opt.update(dict(musicalion_export_user      = settings.user,   
                    musicalion_export_password  = settings.password,
                    musicalion_export_first_usage  = str(settings.firstUsage),
                    musicalion_export_registered  = str(settings.registered),
                    ))
    options.set(opt)

def getSupportedLocales():
    try:
        localeResult = urllib2.urlopen(localeServiceUrl % (site)).read()
    except urllib2.URLError:
        # messageBox('Error','Error reading supported locales')
        return ['de-DE', 'en-US', 'es-ES']
    return [lang + '-' + lang.upper() for lang in localeResult.split()]

def getSupportedExtensions():
    try:
        extensionResult = urllib2.urlopen(fileTypeServiceUrl % (site)).read()
    except urllib2.URLError, ex:
        # messageBox('Error','Error reading supported extension')
        return ['pdf', 'gif', 'cap','mid','mp3','sib','mus','jpg','xml','ly']
    return [extension.lower() for extension in extensionResult.split()]

def showErrorDialog(message):
    messageBox(tr('error'), tr(message), 4, 0, 1)

def extractMessage(ex):
    if hasattr(ex,'strerror'):
        error = ex.strerror
    if error == None:
        if (hasattr(ex, 'args')):
            if len(ex.args) == 1:
                error = str(ex.args[0])
            else:
                error = str(ex.args)
        else:
            error = str(ex)
    return error

def showConfirmationDialog(scoreDir, scoreTitle):
    # show file list/hints
    scorePrefix = scoreTitle.split('.', 1)[0]
    # set up check boxes for each candidate file
    files = []
    checkBoxes = []
    for file in os.listdir(scoreDir):
        extension = os.path.splitext(file)[1].lower()[1:]
        if file.startswith(scorePrefix) and extension in extensions:
            checkBox = CheckBox(file, value=True, width=max(len(file), 40))
            files.append(file)
            checkBoxes.append(checkBox)
    filesBox = VBox(checkBoxes, text=tr('fileBoxTitle'), padding=8)
    hintLabel1 = Label(tr('confirmationMsg') % scoreTitle)
    hintLabel2 = Label(tr('confirmationHint'))
    while True:
        if not (settings.user and settings.password):
            if not showLoginDialog():
                return False
            continue
        # change login box
        changeLoginBox = CheckBox(tr('changeLogin'), value=False, width=40)
        userLabel = Label(tr('user') + ': ' + settings.user, width=40)
        passLabel = Label(tr('password') + ': ' + ''.join(['*' for char in settings.password]), width=40)
        loginBox = VBox([changeLoginBox, userLabel, passLabel], text=tr('loginTitle'), padding=8)
        # content box
        contentBox = VBox([hintLabel1, hintLabel2, loginBox, filesBox], padding=8)
        # dialog
        dialog = Dialog(tr('confirmationTitle'), contentBox)
        if not dialog.run(): 
            return False
        if changeLoginBox.value():
            showLoginDialog()
            changeLoginBox['value'] = False
            continue
        result = []
        for i in range(len(files)):
            if checkBoxes[i].value():
                result.append(files[i])
        return result

def showLoginDialog():
    actionRadio = Radio([tr('loginTitle'), tr('passReminder'), tr('registerUser')], text=tr('loginAction'), value=0, width=40)
    userLabel = Label(tr('user'))
    userEdit = Edit(settings.user, width=30)
    userBox = HBox([userLabel, userEdit], padding=8)
    passLabel = Label(tr('password'))
    passEdit = Edit(settings.password, width=30)
    passBox = HBox([passLabel, passEdit], padding=8)
    emptyLabel = Label('')
    loginBox = VBox([actionRadio, userBox, passBox, emptyLabel], padding=8)
    dialog = Dialog(tr('loginTitle'), loginBox)
    if not dialog.run():
        return False
    settings.user = userEdit.value()
    settings.password = passEdit.value()
    setOptions()
    if (actionRadio.value() == 0):
        return True
    if (actionRadio.value() == 1):
        url = passReminderUrl % (site, lang)
        webbrowser.open(url, True, True)
        return False
    if (actionRadio.value() == 2):
        settings.registered = True
        setOptions()
        url = registerUserUrl % (site, lang)
        webbrowser.open(url, True, True)
        return False
    return False

def login():
    loginFailed = False
    while True:
        if loginFailed or not settings.user:
            if not showLoginDialog():
                return None
        try:
            loginResult = urllib2.urlopen(loginServiceUrl % (site, lang), 
                                          urllib.urlencode(dict(username = settings.user,password = settings.password))).read()
        except urllib2.URLError:
            showErrorDialog('connectionError')
            return None
        if loginResult.startswith('Error:'):
            showErrorDialog('loginError')
            messageBox('Error', loginResult.decode('UTF-8').encode('ISO-8859-1'))
            loginFailed = True
            continue
        return loginResult

def uploadFiles(fileDir, fileNames, sessionId):
    messageBox(tr('uploadTitle'), tr('uploadMsg') % repr(fileNames))
    # set up streaming handler
    setupHttpHandler()
    for fileName in fileNames:
        if not uploadFile(fileDir, fileName, sessionId):
            return False
    return True

class StreamingHTTP(httplib.HTTPConnection):
    # Override the definition of the send method, so that it can stream the data if it is a readable instance
    # This implementation is copied over and adapted from python 2.6
    def send(self, str):
        """Send `str' to the server."""
        self.debuglevel = 1
        if self.sock is None:
            if self.auto_open:
                self.connect()
            else:
                raise NotConnected()
        if self.debuglevel > 0:
            print "send:", repr(str)
        try:
            blocksize=8192
            if hasattr(str,'read'):
                if self.debuglevel > 0: print "sendIng a read()able"
                data=str.read(blocksize)
                while data:
                    self.sock.sendall(data)
                    data=str.read(blocksize)
            else:
                self.sock.sendall(str)
        except socket.error, v:
            if v[0] == 32:      # Broken pipe
                self.close()
            raise

class StreamingHttpHandler(urllib2.HTTPHandler):
    # This implementation is copied over and adapted from python 2.6
    def http_open(self, req):
        host = req.get_host()
        if not host:
            raise URLError('no host given')
        h = StreamingHTTP(host) # will parse host:port
        headers = dict(req.headers)
        headers = dict([(name.title(), val) for name, val in headers.items()])
        try:
            h.request(req.get_method(), req.get_selector(), req.data, headers)
            r = h.getresponse()
        except socket.error, err: # XXX what error?
            raise URLError(err)
        r.recv = r.read
        fp = socket._fileobject(r)
        resp = urllib.addinfourl(fp, r.msg, req.get_full_url())
        resp.code = r.status
        resp.msg = r.reason
        return resp

def setupHttpHandler():
    streaming_support = StreamingHttpHandler()
    # build a new opener that can stream the uploaded file
    opener = urllib2.build_opener(streaming_support)
    # install it
    urllib2.install_opener(opener)

class FormData:
    def __init__(self, fileDir, fileName):
        self.fileName = fileName
        self.fileDir = fileDir
        self.filePath = os.path.join(fileDir, fileName)
        
    def send(self, sessionid):
        CRLF = '\r\n'
        boundary = "---------" + str(long(time.time())) + 'BouNdaRy'
        prefBuf = []
        prefBuf.append('--' + boundary)
        prefBuf.append('Content-Disposition: form-data; name="file_upload"; filename="%s"' % self.fileName.decode('ISO-8859-1').encode('UTF-8'))
        prefBuf.append('Content-Type: application/binary')
        prefBuf.append('')
        prefBuf.append('')
        self.prefix = CRLF.join(prefBuf)
        self.suffix = CRLF + '--' + boundary + '--' + CRLF
        try:
            self.file = open(self.filePath, 'rb')
            fileLength = os.fstat(self.file.fileno()).st_size
        except (AttributeError, OSError,IOError), ex:
            messageBox(tr('uploadFailedTitle'), tr('statFailedMsg') % (repr(self.fileName), extractMessage(ex)))
            return False
        url = uploadServiceUrl % (site, lang, sessionid)
        request = urllib2.Request(url, self)
        request.add_header('Content-Type', 'multipart/form-data; boundary=' + boundary)
        self.bodyLength = len(self.prefix) + fileLength + len(self.suffix)
        self.filePosition = len(self.prefix)
        self.suffixPosition = self.filePosition + fileLength
        # execute the request
        try:
            self.file.seek(0)
            self.position = 0
            request.add_data(self)
            response = urllib2.urlopen(request)
            result = response.read()
            # check the result: error out on error message
            if result.startswith('Error:'):
                raise IOError(result[6:].decode('UTF-8').encode('ISO-8859-1'))
            return True
        except IOError, ex:
            messageBox(tr('uploadFailedTitle'), tr('uploadFailedMsg') % (repr(self.fileName), extractMessage(ex)))
            return False

    def read(self, count):
        # implement the read protocol
        if (self.position < self.filePosition):
            newPosition = min(self.filePosition, self.position + count)
            result = self.prefix[self.position:newPosition]
            self.position = newPosition
            return result
        elif (self.position < self.suffixPosition):
            result = self.file.read(count)
            self.position = self.position + len(result)
            return result
        elif (self.position < self.bodyLength):
            newPosition = min(self.bodyLength, self.position + count)
            result = self.suffix[self.position - self.suffixPosition : newPosition - self.suffixPosition]
            self.position = newPosition
            return result
        else:
            return None
    
    def __len__(self):
        length = int(self.bodyLength)
        return length
    def __nonzero__(self):
        return self.bodyLength > 0

def uploadFile(fileDir, fileName, sessionId):
    formData = FormData(fileDir, fileName)
    return formData.send(sessionId)

def generateDocumentation():
    # loop the supported languages and add one file for all 
    # (we overwrite the files all the time, to make sure they are up to date)
    for locale in getSupportedLocales():
        crtLang = locale[:2]
        htmlFile = os.path.splitext(sys.argv[0])[0] + '-' + crtLang + '.html'
        helpUrl = documentationUrl % (site, crtLang)
        f = file(htmlFile, 'wt')
        f.write('<html><head><meta http-equiv="Refresh" content="1; url=')
        f.write(helpUrl)
        f.write('"></head><body>')
        f.write(translate(locale, 'docRedirect') % ('1', '<a href="' + helpUrl + '">', '</a>'))
        f.write('</body></html>')
        f.close()

def export(score):
    scorePath = score.pathName()
    if not scorePath:
        showErrorDialog('scoreNotSaved')
        return
    (scoreDir, scoreTitle) = os.path.split(scorePath)
    files = showConfirmationDialog(scoreDir, scoreTitle)
    if not files:
        return
    sessionId = login()
    if sessionId == None:
        return
    if not uploadFiles(scoreDir, files, sessionId):
        return
    webbrowser.open(uploadFormUrl % (site, lang, sessionId), True, True)

initSettings()    
lang = bestLocale(getSupportedLocales())[:2]
extensions = getSupportedExtensions()
# generate documentation files
generateDocumentation()
if settings.firstUsage:
    # reset first usage variable
    settings.firstUsage = False
    setOptions()
    # show help in external browser
    url = documentationUrl % (site, lang)
    webbrowser.open(url, True, True)
else:
    if not settings.registered and not settings.user:
        # only call this once
        settings.registered = True
        setOptions()
        # show registration dialog
        url = registerUserUrl % (site, lang)
        webbrowser.open(url, True, True)
    score = activeScore()
    if score:
        export(score)
    else:
        showErrorDialog('noScoreOpen')


