# Copyright (C) 2011 Fog Creek Software
#
# 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, see <http://www.gnu.org/licenses/>.

import os
import sys
import re
import subprocess
import threading
import logging
import json
import urllib2
import tempfile
from datetime import date
from iniparse import ConfigParser
from Queue import Queue, Empty
from zipfile import ZipFile
from PyQt4 import QtGui
from mercurial import ui, url

logger = logging.getLogger('thginithook')
handler = logging.FileHandler('thginithook.log')
handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
logger.addHandler(handler)
logger.setLevel(logging.INFO)

def thginithook():
    try:
        update_check()
        create_kilnhgrc()
        add_symlinks()
        check_cert_exists()
    except Exception, e:
        import traceback
        logger.error(traceback.format_exc())
        logger.error(e)


def add_symlinks():
    if not os.path.exists('/usr/local/bin/hg') or not os.path.exists('/usr/local/bin/thg'):
        result = QtGui.QMessageBox.question(None, 'TortoiseHg', 
            'TortoiseHg has detected that your computer is not configured to support invoking TortoiseHg and/or Mercurial from the terminal.  TortoiseHg can automatically perform this configuration.  If you choose to do so, you may be prompted to enter a user name and password for an account that has administrator access to this computer.  Would you like to configure your computer to support invoking TortoiseHg and Mercurial from the terminal?',
            QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
        if result != QtGui.QMessageBox.Yes:
            return
        try:
            subprocess.check_call(['osascript', '-e', 'do shell script "python copybin.py" with administrator privileges'])
        except subprocess.CalledProcessError:
            pass

def check_cert_exists():
    hascert = False
    hgrc = os.path.expanduser('~/.hgrc')
    if os.path.exists(hgrc):
        file = open(hgrc, 'r')
        lines = file.readlines()
        file.close()
        for line in lines:
            if 'cacerts' in line:
                hascert = True
                break
    if not hascert and os.path.exists('/etc/hg-dummy-cert.pem'):
        file = open(hgrc, 'a')
        file.write('[web]\n')
        file.write('cacerts = /etc/hg-dummy-cert.pem\n')
        file.close()

def current_version():
    from tortoisehg.util import __version__ as thg_version
    version = _normalize_version(thg_version.version)
    return version

def is_newer_version(current, new):
    def comparable_version(ver):
        """
        Takes a dotted version (up to three places) and returns an int version.
        """
        # Make sure we just have numbers and dots.
        ver = _normalize_version(ver)
        l = ver.split('.')
        x = int(l[0]) * 10000000
        if len(l) > 1:
            x += int(l[1]) * 10000
        if len(l) > 2:
            x += int(l[2])
        return x
    return comparable_version(current) < comparable_version(new)

def get_config():
    """
    Gets the prerun config file.

    Returns the ConfigParser object and the path to the file.
    """
    cfg = ConfigParser()
    path = os.path.join(find_resources(), 'prerun.ini')
    cfg.read(path)
    return cfg, path

def save_config(cfg, path):
    """
    Saves the given config object to the given path.
    """
    with open(path, 'w') as fd:
        cfg.write(fd)

def get_user_values(cfg):
    """
    Gets all values from the config's user section and
    returns them as a dictionary.
    """
    return dict([(key, cfg.get('user', key)) for key in cfg.options('user')])

def create_kilnhgrc():
    kilnhgrc_path = os.path.expanduser(os.path.join('~', '.kilnhgrc'))
    hgrc_path = os.path.expanduser(os.path.join('~', '.hgrc'))
    resources = find_resources()
    if resources:
        cfg, cfg_path = get_config()
        app_root = os.path.abspath(find_app_root())
        tortoise_moved = not cfg.has_option('kilnhgrc', 'app_root') or cfg.data.kilnhgrc.app_root != app_root 

        if not os.path.exists(kilnhgrc_path) or tortoise_moved:
            # Either we haven't set up .kilnhgrc or it has moved.
            with open(os.path.join(resources, 'Mercurial-Kiln.ini'), 'r') as fd:
                kilnhgrc = fd.read()
            user_values = get_user_values(cfg)
            user_values['thg_resources'] = resources
            kilnhgrc = kilnhgrc % user_values
            with open(kilnhgrc_path, 'w') as fd:
                fd.write(kilnhgrc)

        if not cfg.getboolean('kilnhgrc', 'added'):
            # Only add it to their .hgrc once.
            if not os.path.exists(hgrc_path):
                with open(hgrc_path, 'w') as fd:
                    fd.write('%include ~/.kilnhgrc\n')
            else:
                with open(hgrc_path, 'r') as fd:
                    hgrc = fd.read()
                if not re.search(r'%include.*\.kilnhgrc', hgrc):
                    with open(hgrc_path, 'w') as fd:
                        fd.write('%include ~/.kilnhgrc\n\n' + hgrc)
            cfg.data.kilnhgrc.added = True
        cfg.data.kilnhgrc.app_root = app_root
        save_config(cfg, cfg_path)

def find_app_root():
    dirs = __file__.split(os.sep)
    if not 'Contents' in dirs:
        return None
    while dirs.pop() != 'Contents':
        pass
    return os.sep + os.path.join(*dirs)

def find_resources():
    app_root = find_app_root()
    if not app_root:
        return None
    return os.path.join(app_root, 'Contents', 'Resources')

def update_check():
    logger.info('Starting update check.')
    app_root = find_app_root()
    if not app_root:
        # We're not in an app bundle. Abort.
        return
    cfg, cfg_path = get_config()
    resources = find_resources()
    update_conf_path = os.path.join(find_resources(), 'update_conf.ini')
    today = str(date.today())
    if cfg.has_option('update', 'autoupdate'):
        if not cfg.getboolean('update', 'autoupdate'):
            return # The user does not want to do updates.
        if cfg.data.update.last_check == today:
            return # We've already checked today, don't check again.
        else:
            cfg.data.update.last_check = today
            save_config(cfg, cfg_path)
    else:
        do_updates = QtGui.QMessageBox.question(None, 'Auto Update?', 'Would you like TortoiseHg to automatically check for updates?', "Don't Check", 'Check Automatically', defaultButtonNumber=1)
        cfg.data.update.autoupdate = (do_updates == 1)
        cfg.data.update.last_check = today
        cfg.data.update.skip_version = '0.0.0'
        save_config(cfg, cfg_path)
        return # We don't want to check for an update on first run.

    opener = url.opener(ui.ui())
    update_url = cfg.data.update.url
    if '%s' in update_url:
        update_url = update_url % current_version()

    logger.info('Getting update JSON from %s.' % update_url)
    try:
        resp = opener.open(update_url, timeout=2)
    except (urllib2.HTTPError, urllib2.URLError):
        # Got a 304, a bad response, or timed out. Nothing we can do but carry on.
        return
    if resp.status != 200:
        # Something is weird, don't try to update.
        return

    update_info = json.load(resp)
    if not is_newer_version(cfg.data.update.skip_version, update_info['version']) \
       or not is_newer_version(current_version(), update_info['version']):
        return

    do_download, do_skip = confirm_download(update_info, cfg)

    if do_download:
        download_url = update_info['url']
        do_update(opener, download_url, cfg, cfg_path)
    elif do_skip:
        # If we did the download, we didn't skip this version, hence the elif.
        cfg.set('update', 'skip_version', update_info['version'])
        save_config(cfg, cfg_path)

def do_update(opener, download_url, cfg, cfg_path):
    import webbrowser
    webbrowser.open(download_url)
    """
    if download_url:
        progress = QtGui.QProgressDialog('Requesting update...', 'Cancel', 0, 1000)
        progress.setModal(True)
        progress.show()
        new_app = download_update(opener, download_url, progress)
        if new_app:
            logger.info('New app: %s' % new_app)
            progress.setLabelText('Installing update...')
            replace_app(new_app, cfg, cfg_path, progress)
            progress.hide()
    """

def confirm_download(info, conf):
    """
    Returns (do_update, skip_version) based on the user's input.
    """
    response = QtGui.QMessageBox.question(None, 'Update', 'An update is available for the Kiln Client Tools.\n\nWould you like to install it?', 'Remind Me Later', 'Skip This Version', 'Download Now', defaultButtonNumber=2)
    if response == 0:
        return False, False
    if response == 1:
        return False, True
    if response == 2:
        return True, False

def get_resp(opener, url, q=None):
    """
    Given an opener and a URL, return the response object,
    either via a queue (for threads) or a regular return.
    """
    resp = opener.open(url)
    if q is not None: q.put(resp)
    return resp

def download_update(opener, download_url, progress):
    logger.info('Starting download...')
    q = Queue()
    resp = None
    t = threading.Thread(target=get_resp, args=(opener, download_url, q))
    t.start()
    logger.info('Getting response...')
    while resp is None:
        try:
            resp = q.get_nowait()
        except Empty:
            QtGui.qApp.processEvents() # Pump the event loop
            if progress.wasCanceled():
                return None
    t.join()

    logger.info('Got response...')
    
    download_progress_label = 'Downloading update...\n(%s / %s)'

    if resp.status == 200:
        downloaded_bytes = 0
        total_bytes = int(resp.info().getheader('Content-Length').strip())
        data_left = total_bytes
        logger.info('Total bytes: %s' % total_bytes)
        progress.setLabelText(download_progress_label % (_display_size(downloaded_bytes), _display_size(total_bytes)))
        tmp_fd, tmp_path = tempfile.mkstemp(suffix='.zip', prefix='thg')
        with os.fdopen(tmp_fd, 'wb') as fd:
            while True:
                data = resp.fp.read(min(data_left, 8192))
                if not data:
                    break
                progress.setValue(1000.0 * downloaded_bytes / total_bytes)
                progress.setLabelText(download_progress_label % (_display_size(downloaded_bytes), _display_size(total_bytes)))
                QtGui.qApp.processEvents()
                if progress.wasCanceled():
                    return None
                fd.write(data)
                downloaded_bytes += len(data)
                data_left -= len(data)
                logger.info(downloaded_bytes)
        logger.info('Download complete')
        progress.setLabelText('Download Complete')
        progress.setValue(1000.0)
        logger.info('Path: %s' % tmp_path)
        return tmp_path
    else:
        progress.cancel()
        QtGui.QMessageBox.critical(None, 'Error', 'Error downloading update.')
        return None

def overwrite_config(cfg, cfg_path):
    """
    Takes an existing config from memory, reads a new one in from disk,
    and then updates the new one with all of the old values.
    """
    new_cfg = ConfigParser()
    new_cfg.read(cfg_path)
    for section in cfg.sections():
        for option in cfg.options(section):
            new_cfg.data[section][option] = cfg.data[section][option]
    save_config(new_cfg, cfg_path)

def replace_app(new_app, cfg, cfg_path, progress):
    def do_unzip(q, new_app, tmpdir):
        try:
            subprocess.check_call(['unzip', new_app, '-d', tmpdir])
            q.put(True)
        except subprocess.CalledProcessError:
            q.put(False)

    if not new_app:
        # We were not passed a real app.
        return

    logger.info('Replacing app...')
    tmpdir = tempfile.mkdtemp()
    app_root = find_app_root()
    bak_root = app_root + '.bak'
    q = Queue()
    threading.Thread(target=do_unzip, args=[q, new_app, tmpdir]).start()
    res = None
    while res is None:
        try:
            res = q.get_nowait()
        except Empty:
            QtGui.qApp.processEvents() # Pump the event loop
            if progress.wasCanceled():
                return
    if not res:
        return
    try:
        os.rename(app_root, bak_root)
        os.rename(os.path.join(tmpdir, 'TortoiseHg.app'), app_root)
        overwrite_config(cfg, cfg_path)
        os.rename(bak_root, os.path.join(tmpdir, 'TortoiseHg.app.bak'))
        logger.info('Restarting the app: %s %s' % (sys.argv[0], sys.argv))
        os.execv(sys.argv[0], sys.argv)
        QtGui.qApp.quit()
    except Exception, e:
        import traceback
        QtGui.QMessageBox.critical(None, 'Error', 'Error extracting update.')
        logger.error(traceback.format_exc())
        logger.error(e)
        if os.path.exists(bak_root):
            # Restore the backup
            os.rename(bak_root, app_root)

def _display_size(x):
    if x > 1024.0 * 1024.0 / 2.0:
        return '%.1f MB' % (x / (1024.0 * 1024.0))
    elif x > 1024.0 / 2.0:
        return '%.1f kb' % (x / 1024.0)
    else:
        return '%d bytes' % x

def _normalize_version(ver):
    return re.sub(r'[^0-9.].*', '', ver).strip('.')

