#!/usr/bin/env python

""" Script to insert jfniki releases into Freenet.

    Copyright (C) 2011 Darrell Karbott

    This library 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.0 of the License, or (at your option) any later version.

    This library 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 library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

    Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
"""

# This script isn't really for public consumption.
# It is brittle and not fully debugged.
# It assumes you have hg infocalypse installed and configured.
#
# POSSIBLE BUG: ANONYMITY ISSUE: This script may leak the *nix user id into the .jar file.
#      Need to audit.
#      DO NOT RUN IT if this concerns you.
#
import os
import shutil
import subprocess

from binascii import hexlify

from mercurial import ui, hg, commands

from insert_files import insert_files
from minimalfms import get_connection, send_msgs

############################################################

# For testing this script.
#JUST_STAGE = True
JUST_STAGE = False

# CAUTION: This directory is recursively deleted!
STAGING_DIR = '/tmp/staging'

FCP_HOST = '127.0.0.1'
FCP_PORT = 19481

FMS_HOST = '127.0.0.1'
FMS_PORT =  11119

FMS_ID = 'djk'
FMS_GROUP = 'sites'

# REQUIRES: must match name in freesite.cfg. LATER: fix.
SITE_NAME = 'jfniki_releases'

PUBLIC_SITE = "USK@kRM~jJVREwnN2qnA8R0Vt8HmpfRzBZ0j4rHC2cQ-0hw," + \
              "2xcoQVdQLyqfTpF2DpkdUIbHFCeL4W~2X1phUYymnhM,AQACAAE/%s/%%d/" % \
              SITE_NAME

############################################################
# Indexes of referenced USK sites

FREENET_DOC_WIKI_IDX = 56
FNIKI_IDX = 85
REPO_IDX = 19

############################################################

THIS_FILES_DIR = os.path.abspath(os.path.dirname(__file__))

REPO_DIR = os.path.abspath(os.path.join(THIS_FILES_DIR, '..'))
FREENET_JAR = os.path.abspath(os.path.join(THIS_FILES_DIR, '../alien/libs/freenet.jar'))
RELEASE_NOTES = os.path.abspath(os.path.join(THIS_FILES_DIR, '../doc/latest_release.txt'))
INDEX_HTML = os.path.abspath(os.path.join(THIS_FILES_DIR, 'generated_freesite/index.html'))
INDEX_HTML_TEMPLATE = os.path.abspath(os.path.join(THIS_FILES_DIR, 'index_template.html'))
FMS_MESSAGE_TEMPLATE = os.path.abspath(os.path.join(THIS_FILES_DIR, 'fms_message_template.txt'))

############################################################

def zip_source(staging_dir, source_dir, zip_file_name):
    subprocess.check_call(['/usr/bin/zip',
                           '-r',
                           '-9', # best compression
                           zip_file_name,
                           source_dir],
                          # LATER: Better way to supress full path in zip?
                          cwd=staging_dir)

def stage_release():
    # LATER: check for uncommitted changes
    ui_ = ui.ui()
    repo = hg.repository(ui_, REPO_DIR)

    # Get current head.
    heads = [hexlify(repo[head].node())[:12] for head in repo.heads()]
    assert len(heads) == 1 # Don't try to handle multiple heads
    head = heads[0]

    jar_name = "jfniki.%s.jar" % head
    zip_name = "jfniki.%s.zip" % head
    export_dir_name = "jfniki.%s" % head

    zip_file_name = "%s/%s" % (STAGING_DIR, zip_name)
    jar_file_name = "%s/%s" % (STAGING_DIR, jar_name)

    # scrub staging directory
    try:
        shutil.rmtree(STAGING_DIR)
    except:
        pass

    os.makedirs(STAGING_DIR)

    # dump clean source to staging
    dest =  "%s/%s" % (STAGING_DIR, export_dir_name)

    # TRICKY: Had to put a print in the implementation of command.archive to figure
    #         out required default opts.
    # {'rev': '', 'no_decode': None, 'prefix': '', 'exclude': [], 'include': [], 'type': ''}
    commands.archive(ui_, repo, dest,
                     rev='', no_decode=None, prefix='', exclude=[], include=[], type='')


    # remove origin tarballs to save space
    shutil.rmtree("%s/alien/origins/" % dest)

    # zip up the source.
    zip_source(STAGING_DIR, export_dir_name, zip_file_name)

    # cp freenet.jar required for build
    os.makedirs("%s/%s/%s" % (STAGING_DIR, export_dir_name, "alien/libs"))
    shutil.copyfile(FREENET_JAR, "%s/%s/%s" % (STAGING_DIR, export_dir_name, "alien/libs/freenet.jar"))

    # build jar
    result = subprocess.check_call(["/usr/bin/ant",
                                    "-buildfile",
                                    "%s/%s/build.xml" % (STAGING_DIR, export_dir_name)])
    print "ant result code: %d" % result

    # copy jar with name including the hg rev.
    shutil.copyfile("%s/%s/%s" % (STAGING_DIR, export_dir_name, "build/jar/jfniki.jar"), jar_file_name)

    print
    print "SUCCESSFULLY STAGED:"
    print jar_file_name
    print zip_file_name
    print
    return (head, jar_file_name, zip_file_name)


def simple_templating(text, substitutions):
    for variable in substitutions:
        text = text.replace(variable, str(substitutions[variable]))
    assert text.find("__") == -1 # Catch unresolved variables
    return text

def latest_site_index(repo): # C&P: wikibot.py
    """ Read the latest known freesite index out of the hg changelog. """
    for tag, dummy in reversed(repo.tagslist()):
        if tag.startswith('I_'):
            return int(tag.split('_')[1])
    return -1

def tag_site_index(ui_, repo, index=None): # C&P: wikibot.py
    """ Tag the local repository with a freesite index. """
    if index is None:
        index = latest_site_index(repo) + 1
    commands.tag(ui_, repo, 'I_%i' % index)

############################################################
# ATTRIBUTION:
# http://wiki.python.org/moin/EscapingHtml
HTML_ESCAPE_TABLE = {
    "&": "&",
    '"': """,
    "'": "'",
    ">": ">",
    "<": "<"}

def html_escape(text):
    """Produce entities within text."""
    return "".join(HTML_ESCAPE_TABLE.get(c,c) for c in text)

############################################################

def update_html(head, jar_chk, zip_chk):
    ui_ = ui.ui()
    repo = hg.repository(ui_, REPO_DIR)
    site_usk = PUBLIC_SITE % (latest_site_index(repo) + 1)

    html = simple_templating(open(INDEX_HTML_TEMPLATE).read(),
                             {'__HEAD__':head,
                              '__JAR_CHK__': jar_chk,
                              '__SRC_CHK__': zip_chk,
                              '__RELEASE_NOTES__' : html_escape(open(RELEASE_NOTES).read()),
                              '__SITE_USK__': site_usk,
                              '__INDEX_FDW__': FREENET_DOC_WIKI_IDX,
                              '__INDEX_FNIKI__': FNIKI_IDX,
                              '__INDEX_REPO__': REPO_IDX,
                              })

    updated = open(INDEX_HTML, 'w')
    updated.write(html)
    updated.close()

    commit_msg = "index.html:%s" % head
    commands.commit(ui_, repo, pat = (INDEX_HTML, ),
                    include = [], addremove = None, close_branch = None,
                    user = '', date = '',
                    exclude = [], logfile = '',
                    message = commit_msg)
    tag_site_index(ui_, repo)

# Insert the latest tagged freesite version into freenet.
# NOTE: depends on state set in freesite.cfg
# REQUIRES: 'I_<num>' tag exists in the repo.
# REQUIRES: hg infocalypse is installed and configured.
def insert_freesite():
    print REPO_DIR
    ui_ = ui.ui()
    repo = hg.repository(ui_, REPO_DIR)
    target_index = latest_site_index(repo)
    assert target_index >= 0

    # BUG: There are case when this can fail with error code == 0
    #      e.g. when private key can't be read.
    # DCI: Test. does fn-putsite set error code on failure?
    subprocess.check_call(["/usr/bin/hg",
                           "-R",
                           REPO_DIR,
                           "fn-putsite",
                           "--index",
                           str(target_index)])

    # LATER: Do better. Parse request URI from output.
    return PUBLIC_SITE % target_index, target_index

def send_fms_notification(site_uri, target_index, head, jar_chk, zip_chk):

    connection = get_connection(FMS_HOST, FMS_PORT, FMS_ID)

    msg = simple_templating(open(FMS_MESSAGE_TEMPLATE).read(),
                             {'__HEAD__':head,
                              '__JAR_CHK__': jar_chk,
                              '__SRC_CHK__': zip_chk,
                              '__SITE_USK__' : site_uri,
                              '__RELEASE_NOTES__' : open(RELEASE_NOTES).read(),
                              })

    send_msgs(connection,
              ((FMS_ID, FMS_GROUP,
                "jfniki releases #%d" % target_index,
                msg),))

    print "Sent FMS notification to: %s" % FMS_GROUP

def release():
    print
    print "If you haven't read the warning about ANONYMITY ISSUES"
    print "with this script, now might be a good time to hit Ctrl-C."
    print
    print
    print "RELEASE NOTES:"
    print open(RELEASE_NOTES).read()
    print
    print "------------------------------------------------------------"

    head, jar_file, zip_file = stage_release()

    if JUST_STAGE:
        return

    jar_chk, zip_chk = insert_files(FCP_HOST, FCP_PORT, [jar_file, zip_file])
    update_html(head, jar_chk, zip_chk)
    site_uri, target_index = insert_freesite()
    send_fms_notification(site_uri, target_index, head, jar_chk, zip_chk)

    print
    print "Success!"


if __name__ == "__main__":
    release()