#!/usr/bin/python

# Copyright (C) 2010 Canonical Ltd
#
# 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.
#
# Written by: Chris Coulson <chris.coulson@canonical.com>

from optparse import OptionParser
import glib
import gtk
import os.path
import ConfigParser
import re
import shutil

def get_last_version_from_compat_ini (subprofile):
    if subprofile == None:
        return None

    compat_ini = ConfigParser.ConfigParser ()
    compat_ini.read (os.path.join (subprofile, 'compatibility.ini'))

    try:
        version = re.sub (r'_.*', '', compat_ini.get ('Compatibility', 'LastVersion'))
        return version if re.match('[0-9]', version[0]) != None else None
    except:
        return None

def get_series_from_last_version (version):
    series = re.sub (r'[ab].*', '', re.sub (r'pre.*', '', version)).split ('.')
    # FIXME: The "series" returned should have the same format and length as that passed
    # on the command line, rather than hard-coding the first 2 digits
    return series[0] + '.' + series[1]

class Enum (set):
    def __getattr__ (self, name):
        if name in self:
            return name
        raise AttributeError

MigrationType = Enum (["NEW", "BETA"])
CompareResultType = Enum (["LT", "EQ", "GT"])
MigrationActionType = Enum (["KEEP", "IMPORT", "ASKLATER"])

class ProfileVersion:
    def __init__ (this, version):
        this.release = re.sub (r'[ab].*', '', re.sub (r'pre.*', '', version))
        this.prerelease = False if len (re.findall ('pre', version)) == 0 else True
        this.beta_milestone = None if len (re.findall ('b', version)) == 0 else re.sub(r'.*b', '', re.sub(r'pre.*' , '', version))
        this.alpha_milestone = None if len (re.findall ('a', version)) == 0 else re.sub(r'.*a', '', re.sub(r'pre.*' , '', version))
        this.raw = version

    def compare (this, version):
        this_version_spl = this.release.split ('.')
        comp_version_spl = version.release.split ('.')

        for i in range (len (this_version_spl)):
            if i == len (comp_version_spl):
                return CompareResultType.GT
            if i == (len (this_version_spl) - 1) and i < (len (comp_version_spl) - 1) and int (this_version_spl[i]) == int (comp_version_spl[i]):
                return CompareResultType.LT
            if int (this_version_spl[i]) > int (comp_version_spl[i]):
                return CompareResultType.GT
            elif int (this_version_spl[i]) < int (comp_version_spl[i]):
                return CompareResultType.LT

        if this.alpha_milestone != None:
            if version.alpha_milestone == None:
                return CompareResultType.LT
            elif int (this.alpha_milestone) > int (version.alpha_milestone):
                return CompareResultType.GT
            elif int (this.alpha_milestone) < int (version.alpha_milestone):
                return CompareResultType.LT
        elif this.beta_milestone != None:
            if version.beta_milestone == None and version.alpha_milestone == None:
                return CompareResultType.LT
            elif version.alpha_milestone != None:
                return CompareResultType.GT
            elif int (this.beta_milestone) > int (version.beta_milestone):
                return CompareResultType.GT
            elif int (this.beta_milestone) < int (version.beta_milestone):
                return CompareResultType.LT
        elif version.alpha_milestone != None or version.beta_milestone != None:
            return CompareResultType.GT

        if this.prerelease == True and version.prerelease == False:
            return CompareResultType.LT
        elif this.prerelease == False and version.prerelease == True:
            return CompareResultType.GT
        else:
            return CompareResultType.EQ

class ProfileFolder:
    def __init__ (this, path):
        this.path = path
        this.series = None
        this.version = None
        this.last_version_string = None
        this.subprofiles = list ()
        this.default_subprofile = None

        # If there is no profiles.ini, then ignore this folder
        this.is_profile = os.path.isfile (os.path.join (this.path, 'profiles.ini'))

        if this.is_profile == True:
            this.build_list_of_subprofiles ()
            this.find_last_version_from_profile ()
            if this.last_version_string != None:
                series_string = get_series_from_last_version (this.last_version_string)
                if series_string != None:
                    this.series = ProfileVersion (series_string)
                else:
                    this.is_profile = False
            else:
                this.is_profile = False

    def build_list_of_subprofiles (this):
        # Parse profiles.ini for the list of "subprofiles" in this profile.
        profiles_ini = ConfigParser.ConfigParser ()
        profiles_ini.read (os.path.join (this.path, 'profiles.ini'))
        has_default = False
        for i in profiles_ini.sections ():
            if i == 'General':
                if profiles_ini.has_option (i, 'StartWithLastProfile') and profiles_ini.getint (i, 'StartWithLastProfile') == 1:
                    has_default = True
                continue

            if profiles_ini.has_option (i, 'Path'):
                ppath = profiles_ini.get (i, 'Path')
                if os.path.isfile (os.path.join (this.path, ppath, 'compatibility.ini')):
                    this.subprofiles.append (os.path.join (this.path, ppath))

                    if profiles_ini.has_option (i, 'Default') == True and profiles_ini.getint (i, 'Default') == 1:
                        this.default_subprofile = os.path.join (this.path, ppath)

        if has_default == False:
            this.default_subprofile = None

    def find_last_version_from_profile (this):
        last_version_string = None

        if this.default_subprofile != None:
            # If there is a default subprofile and StartWithLastProfile == 1, then take the version from there
            this.last_version_string = get_last_version_from_compat_ini (this.default_subprofile)

        if this.last_version_string != None:
            this.version = ProfileVersion (this.last_version_string)
            return

        # If there is no default subprofile, iterate over all profiles and find the highest version
        for i in this.subprofiles:
            last_version_string = get_last_version_from_compat_ini (i)
            if last_version_string != None:
                version = ProfileVersion (last_version_string)
                if this.version == None or this.version.compare (version) != CompareResultType.GT:
                    this.version = version
                    this.last_version_string = last_version_string

    def is_suitable (this, name, new_series, last_series=None):
        basename = re.sub (r'-.*', '', os.path.basename (this.path))
        if name != basename:
            return False

        # If this profile is from an application that is newer than the
        # one running now, it isn't suitable
        if this.series.compare (new_series) == CompareResultType.GT:
            return False

        # If this profile is from an application that is older than
        # the application that last ran the profile we are migrating, then
        # ignore this profile
        if last_series != None and this.series.compare (last_series) != CompareResultType.GT:
            return False

        return True

class ProfileMigrator:
    def __init__ (this, options):
        this.new_series = ProfileVersion (options.series)
        this.profile = ProfileFolder (options.profile)
        this.candidates = list ()
        this.last_series = None
        this.basename = options.name
        this.name = os.path.basename (this.profile.path)
        this.updated = False
        this.ask_again = False
        this.displayname = options.displayname
        this.action = MigrationActionType.ASKLATER

        if not os.path.isdir (os.path.dirname (this.profile.path)):
            # The parent directory doesn't exist, so there's nothing to do
            exit (0)

        if os.path.isdir (this.profile.path) and this.profile.is_profile:
            # This profile already exists, so we do the beta migration routine
            this.migrate_type = MigrationType.BETA
            this.last_series = this.profile.series
        elif not os.path.isdir (this.profile.path):
            # There is no profile yet, so we look for an existing profile
            # to base our new profile on
            this.migrate_type = MigrationType.NEW
            if os.path.isfile (this.profile.path + ".last-version"):
                # In this case, the user deleted their profile directory.
                # They want a fresh profile, so give them this
                update_stamp (options.profile + '.last-version', options.series)
                exit (0)
        else:
            # There is a folder where we wan't our profile, but it doesn't
            # look like an existing profile. Just bail
            update_stamp (options.profile + '.last-version', options.series)
            exit (0)

    def check_candidates (this):
        for f in os.listdir (os.path.dirname (this.profile.path)):
            (root, ext) = os.path.splitext (f)
            if len (re.findall ('abandoned', ext)) > 0:
                continue

            if len (re.findall ('replaced', ext)) > 0:
                continue

            profile = ProfileFolder (os.path.join (os.path.dirname (this.profile.path), f))
            if not profile.is_profile:
                continue

            if profile.is_suitable (this.basename, this.new_series, this.last_series):
                this.candidates.append (profile)

        if len (this.candidates) == 0:
            this.updated = True
            gtk.main_quit ()
            return

        if this.migrate_type == MigrationType.NEW:
            this.create_new_profile ()
        else:
            this.show_dialog ()

    def select_profile (this):
        selected = None
        # Iterate over the list of candidate profiles and pick the newest
        for profile in this.candidates:
            if (selected != None and profile.version.compare (selected.version) == CompareResultType.GT) or selected == None:
                selected = profile

        return selected

    def create_new_profile (this):
        selected = this.select_profile ()

        if selected == None:
            gtk.main_quit ()
            return

        # FIXME: Don't copy an in-use profile
        # FIXME: What if the destination folder exists already?
        shutil.copytree (src=selected.path, dst=this.profile.path, symlinks=True)
        this.updated = True
        gtk.main_quit ()
        return

    def show_dialog (this):
        selected = this.select_profile ()

        if selected == None:
            gtk.main_quit ()
            return

        title = this.displayname + " Profile Migration"
        this.dialog = gtk.Dialog (title=title, buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))

        msg_box = gtk.HBox (False, 12)
        this.dialog.vbox.pack_start (msg_box, False, False, 0)

        img = gtk.Image ()
        img.set_from_stock (gtk.STOCK_DIALOG_QUESTION, gtk.ICON_SIZE_DIALOG)
        img.set_alignment (0.5, 0.5)
        msg_box.pack_start (img, False, False, 0)

        label_box = gtk.VBox (False, 12)
        msg_box.pack_start (label_box, True, True, 0)

        label_txt = "<b><big> Would you like to import your settings from " + this.displayname + " " + selected.series.raw + "?</big></b>"
        label = gtk.Label ()
        label.set_markup (label_txt)
        label.set_alignment (0, 0)
        label_box.pack_start (label, False, False, 5)

        label_txt = "It seems that you have used " + this.displayname + " " + selected.series.raw + " before. " + \
            "The bookmarks and other settings for " + this.displayname + " " + selected.series.raw + " are currently stored " + \
            "in a separate profile. It is possible to import these settings in to your current " + this.displayname + " " + this.last_series.raw + " profile."
        label = gtk.Label (label_txt)
        label.set_line_wrap (True)
        label.set_alignment (0.0, 0.0)
        label_box.pack_start (label, True, True, 0)

        label_txt = "What would you like to do?"
        label = gtk.Label (label_txt)
        label.set_line_wrap (True)
        label.set_alignment (0.0, 0.0)
        label_box.pack_start (label, True, True, 0)

        label_txt = "Keep my bookmarks and settings from " + this.displayname + " " + this.last_series.raw + " (version " + this.profile.version.raw + ")"
        button = gtk.RadioButton (None, label_txt)
        button.connect ("toggled", this.toggle_callback, MigrationActionType.KEEP)
        button.set_active (True)
        button.toggled ()
        this.dialog.vbox.pack_start (button, True, True, 0)

        label_txt = "Import my bookmarks and settings from " + this.displayname + " " + selected.series.raw + " (version " + selected.version.raw + ")"
        button = gtk.RadioButton (button, label_txt)
        button.connect ("toggled", this.toggle_callback, MigrationActionType.IMPORT)
        this.dialog.vbox.pack_start (button, True, True, 0)

        label_txt = "I can't decide, so ask me again later"
        button = gtk.RadioButton (button, label_txt)
        button.connect ("toggled", this.toggle_callback, MigrationActionType.ASKLATER)
        this.dialog.vbox.pack_start (button, True, True, 0)

        this.dialog.set_border_width (5)
        msg_box.set_border_width (5)
        this.dialog.vbox.set_spacing (14)
        this.dialog.action_area.set_border_width (5)
        this.dialog.action_area.set_spacing (6)

        this.dialog.connect ("response", this.dialog_response, selected)

        this.dialog.set_icon_name (this.name)
        this.dialog.set_wmclass (this.displayname, this.displayname)

        this.dialog.show_all ()

    def dialog_response (this, dialog, response_id, selected):
        this.dialog.hide_all ()
        this.dialog.destroy ()

        if response_id != gtk.RESPONSE_ACCEPT:
            gtk.main_quit ()
            return

        if this.action == MigrationActionType.KEEP:
            this.updated = True
            gtk.main_quit ()
        elif this.action == MigrationActionType.ASKLATER:
            this.ask_again = True
            gtk.main_quit()
        elif this.action == MigrationActionType.IMPORT:
            this.import_profile (selected)
            this.updated = True
        else:
            raise RuntimeError

    def import_profile (this, selected):
        if selected == None:
            raise RuntimeError

        # FIXME: What if .replaced already exists?
        shutil.move (this.profile.path, this.profile.path + '.replaced')
        # FIXME: What if the profile is in use?
        shutil.copytree (src=selected.path, dst=this.profile.path, symlinks=True)

        gtk.main_quit()

    def toggle_callback (this, widget, data=None):
        if widget.get_active ():
            this.action = data

    def main (this):
        if this.migrate_type == MigrationType.BETA and this.new_series == this.last_series:
            return False

        glib.idle_add (this.check_candidates)
        gtk.main ()

        return this.updated

def test_compare ():
    matrix = {   "3.6" : [ CompareResultType.GT, CompareResultType.GT, CompareResultType.GT ],
        "4.1" : [ CompareResultType.LT, CompareResultType.LT, CompareResultType.LT ],
        "4.0" : [ CompareResultType.EQ, CompareResultType.LT, CompareResultType.LT ],
        "4.0.1" : [ CompareResultType.LT, CompareResultType.LT, CompareResultType.LT ],
        "3.6.12" : [ CompareResultType.GT, CompareResultType.GT, CompareResultType.GT ],
        "4.0b8" : [ CompareResultType.GT, CompareResultType.LT, CompareResultType.EQ ],
        "4.0b8pre" : [ CompareResultType.GT, CompareResultType.EQ, CompareResultType.GT ],
        "4.0a1" : [ CompareResultType.GT, CompareResultType.GT, CompareResultType.GT ],
        "4.0a2pre" : [ CompareResultType.GT, CompareResultType.GT, CompareResultType.GT ],
        "4" : [ CompareResultType.GT, CompareResultType.GT, CompareResultType.GT ],
        "3" : [ CompareResultType.GT, CompareResultType.GT, CompareResultType.GT ],
        "5" : [ CompareResultType.LT, CompareResultType.LT, CompareResultType.LT ]}

    baseline1 = ProfileVersion ("4.0")
    baseline2 = ProfileVersion ("4.0b8pre")
    baseline3 = ProfileVersion ("4.0b8")

    has_fail = False

    for i in matrix:
        exp1, exp2, exp3 = matrix[i]
        res1 = baseline1.compare (ProfileVersion (i))
        res2 = baseline2.compare (ProfileVersion (i))
        res3 = baseline3.compare (ProfileVersion (i))

        if res1 != exp1:
            print "***FAIL: " + baseline1.raw + " against " + i + " - expected " + exp1 + ", got " + res1 + " ***"
            has_fail = True
        else:
            print "PASS: " + baseline1.raw + " against " + i + " - got " + res1
        if res2 != exp2:
            print "***FAIL: " + baseline2.raw + " against " + i + " - expected " + exp2 + ", got " + res2 + " ***"
            has_fail = True
        else:
            print "PASS: " + baseline2.raw + " against " + i + " - got " + res2
        if res3 != exp3:
            print "***FAIL: " + baseline3.raw + " against " + i + " - expected " + exp3 + ", got " + res3 + " ***"
            has_fail = True
        else:
            print "PASS: " + baseline3.raw + " against " + i + " - got " + res3

    if has_fail == True:
        exit (1)


def update_stamp (stampfile, series):
    fd = open (stampfile, 'w')
    fd.write (series)
    fd.close()

def parse_argv ():
    parser = OptionParser ()
    parser.add_option ("-s", type="string", dest="series")
    parser.add_option ("-p", type="string", dest="profile")
    parser.add_option ("-t", action="store_true", dest="test_compare")
    parser.add_option ("-d", type="string", dest="displayname")
    parser.add_option ("-a", type="string", dest="name")
    return parser.parse_args ()

if __name__ == '__main__':
    (options, args) = parse_argv ()

    if options.test_compare == True:
        test_compare ()
        exit (0)

    migrator = ProfileMigrator (options)
    if (migrator.main () == True or not os.path.isfile (options.profile + '.last-version')):
        update_stamp (options.profile + '.last-version', options.series if migrator.ask_again == False else migrator.last_series.raw)

    exit (0)

