#
# BitBake Graphical GTK User Interface
#
# Copyright (C) 2011        Intel Corporation
#
# Authored by Joshua Lock <josh@linux.intel.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# 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 gtk
import gobject
import re

class BuildRep(gobject.GObject):

    def __init__(self, userpkgs, allpkgs, base_image=None):
        gobject.GObject.__init__(self)
        self.base_image = base_image
        self.allpkgs = allpkgs
        self.userpkgs = userpkgs

    def loadRecipe(self, pathname):
        contents = []
        packages = ""
        base_image = ""

        with open(pathname, 'r') as f:
            contents = f.readlines()

        pkg_pattern = "^\s*(IMAGE_INSTALL)\s*([+=.?]+)\s*(\".*?\")"
        img_pattern = "^\s*(require)\s+(\S+.bb)"

        for line in contents:
            matchpkg = re.search(pkg_pattern, line)
            matchimg = re.search(img_pattern, line)
            if matchpkg:
                packages = packages + matchpkg.group(3).strip('"')
            if matchimg:
                base_image = os.path.basename(matchimg.group(2)).split(".")[0]

        self.base_image = base_image
        self.userpkgs = packages

    def writeRecipe(self, writepath, model):
        template = """
# Recipe generated by the HOB

require %s

IMAGE_INSTALL += "%s"
"""

        empty_template = """
# Recipe generated by the HOB

inherit core-image

IMAGE_INSTALL = "%s"
"""
        if self.base_image and not self.base_image == "empty":
            meta_path = model.find_image_path(self.base_image)
            recipe = template % (meta_path, self.userpkgs)
        else:
            recipe = empty_template % self.allpkgs

        if os.path.exists(writepath):
            os.rename(writepath, "%s~" % writepath)

        with open(writepath, 'w') as r:
            r.write(recipe)

        return writepath

class TaskListModel(gtk.ListStore):
    """
    This class defines an gtk.ListStore subclass which will convert the output
    of the bb.event.TargetsTreeGenerated event into a gtk.ListStore whilst also
    providing convenience functions to access gtk.TreeModel subclasses which
    provide filtered views of the data.
    """
    (COL_NAME, COL_DESC, COL_LIC, COL_GROUP, COL_DEPS, COL_BINB, COL_TYPE, COL_INC, COL_IMG, COL_PATH, COL_PN) = range(11)

    __gsignals__ = {
        "tasklist-populated" : (gobject.SIGNAL_RUN_LAST,
                                gobject.TYPE_NONE,
                                ()),
        "contents-changed"   : (gobject.SIGNAL_RUN_LAST,
                                gobject.TYPE_NONE,
                                (gobject.TYPE_INT,)),
        "image-changed"      : (gobject.SIGNAL_RUN_LAST,
                                gobject.TYPE_NONE,
                                (gobject.TYPE_STRING,)),
        }

    """
    """
    def __init__(self):
        self.contents = None
        self.tasks = None
        self.packages = None
        self.images = None
        self.selected_image = None
        
        gtk.ListStore.__init__ (self,
                                gobject.TYPE_STRING,
                                gobject.TYPE_STRING,
                                gobject.TYPE_STRING,
                                gobject.TYPE_STRING,
                                gobject.TYPE_STRING,
                                gobject.TYPE_STRING,
                                gobject.TYPE_STRING,
                                gobject.TYPE_BOOLEAN,
                                gobject.TYPE_BOOLEAN,
                                gobject.TYPE_STRING,
                                gobject.TYPE_STRING)

    """
    Helper method to determine whether name is a target pn
    """
    def non_target_name(self, name):
        if ('-native' in name) or ('-cross' in name) or name.startswith('virtual/'):
            return True
        return False

    def contents_changed_cb(self, tree_model, path, it=None):
        pkg_cnt = self.contents.iter_n_children(None)
        self.emit("contents-changed", pkg_cnt)

    def contents_model_filter(self, model, it):
        if not model.get_value(it, self.COL_INC) or model.get_value(it, self.COL_TYPE) == 'image':
            return False
        name = model.get_value(it, self.COL_NAME)
        if self.non_target_name(name):
            return False
        else:
            return True

    """
    Create, if required, and return a filtered gtk.TreeModel
    containing only the items which are to be included in the
    image
    """
    def contents_model(self):
        if not self.contents:
            self.contents = self.filter_new()
            self.contents.set_visible_func(self.contents_model_filter)
            self.contents.connect("row-inserted", self.contents_changed_cb)
            self.contents.connect("row-deleted", self.contents_changed_cb)
        return self.contents
    
    """
    Helper function to determine whether an item is a task
    """
    def task_model_filter(self, model, it):
        if model.get_value(it, self.COL_TYPE) == 'task':
            return True
        else:
            return False

    """
    Create, if required, and return a filtered gtk.TreeModel
    containing only the items which are tasks
    """
    def tasks_model(self):
        if not self.tasks:
            self.tasks = self.filter_new()
            self.tasks.set_visible_func(self.task_model_filter)
        return self.tasks

    """
    Helper function to determine whether an item is an image
    """
    def image_model_filter(self, model, it):
        if model.get_value(it, self.COL_TYPE) == 'image':
            return True
        else:
            return False

    """
    Create, if required, and return a filtered gtk.TreeModel
    containing only the items which are images
    """
    def images_model(self):
        if not self.images:
            self.images = self.filter_new()
            self.images.set_visible_func(self.image_model_filter)
        return self.images

    """
    Helper function to determine whether an item is a package
    """
    def package_model_filter(self, model, it):
        if model.get_value(it, self.COL_TYPE) != 'package':
            return False
        else:
            name = model.get_value(it, self.COL_NAME)
            if self.non_target_name(name):
                return False
            return True

    """
    Create, if required, and return a filtered gtk.TreeModel
    containing only the items which are packages
    """
    def packages_model(self):
        if not self.packages:
            self.packages = self.filter_new()
            self.packages.set_visible_func(self.package_model_filter)
        return self.packages

    """
    The populate() function takes as input the data from a
    bb.event.TargetsTreeGenerated event and populates the TaskList.
    Once the population is done it emits gsignal tasklist-populated
    to notify any listeners that the model is ready
    """
    def populate(self, event_model):
        # First clear the model, in case repopulating
        self.clear()
        for item in event_model["pn"]:
            atype = 'package'
            name = item
            summary = event_model["pn"][item]["summary"]
            lic = event_model["pn"][item]["license"]
            group = event_model["pn"][item]["section"]
            filename = event_model["pn"][item]["filename"]
            if ('task-' in name):
                atype = 'task'
            elif ('-image-' in name):
                atype = 'image'

            # Create a combined list of build and runtime dependencies and
            # then remove any duplicate entries and any entries for -dev
            # packages
            depends = event_model["depends"].get(item, [])
            rdepends = event_model["rdepends-pn"].get(item, [])
            packages = {}
            for pkg in event_model["packages"]:
                if event_model["packages"][pkg]["pn"] == name:
                    deps = []
                    deps.extend(depends)
                    deps.extend(event_model["rdepends-pkg"].get(pkg, []))
                    deps.extend(rdepends)
                    deps = self.squish(deps)
                    # rdepends-pn includes pn-dev
                    if ("%s-dev" % item) in deps:
                        deps.remove("%s-dev" % item)
                    # rdepends-on includes pn
                    if pkg in deps:
                        deps.remove(pkg)
                    packages[pkg] = deps

            for p in packages:
                self.set(self.append(), self.COL_NAME, p, self.COL_DESC, summary,
                         self.COL_LIC, lic, self.COL_GROUP, group,
                         self.COL_DEPS, " ".join(packages[p]), self.COL_BINB, "",
                         self.COL_TYPE, atype, self.COL_INC, False,
                         self.COL_IMG, False, self.COL_PATH, filename,
                         self.COL_PN, item)

	self.emit("tasklist-populated")

    """
    Load a BuildRep into the model
    """
    def load_image_rep(self, rep):
        # Unset everything
        it = self.get_iter_first()
        while it:
            path = self.get_path(it)
            self[path][self.COL_INC] = False
            self[path][self.COL_IMG] = False
            it = self.iter_next(it)

        # Iterate the images and disable them all
        it = self.images.get_iter_first()
        while it:
            path = self.images.convert_path_to_child_path(self.images.get_path(it))
            name = self[path][self.COL_NAME]
            if name == rep.base_image:
                self.include_item(path, image_contents=True)
            else:
                self[path][self.COL_INC] = False
            it = self.images.iter_next(it)

        # Mark all of the additional packages for inclusion
        packages = rep.userpkgs.split(" ")
        it = self.get_iter_first()
        while it:
            path = self.get_path(it)
            name = self[path][self.COL_NAME]
            if name in packages:
                self.include_item(path, binb="User Selected")
                packages.remove(name)
            it = self.iter_next(it)

        self.emit("image-changed", rep.base_image)

    """
    squish lst so that it doesn't contain any duplicate entries
    """
    def squish(self, lst):
        seen = {}
        for l in lst:
            seen[l] = 1
        return seen.keys()

    """
    Mark the item at path as not included
    NOTE:
    path should be a gtk.TreeModelPath into self (not a filtered model)
    """
    def remove_item_path(self, path):
        self[path][self.COL_BINB] = ""
        self[path][self.COL_INC] = False

    """
    Recursively called to mark the item at opath and any package which
    depends on it for removal.
    NOTE: This method dumbly removes user selected packages and since we don't
    do significant reverse dependency tracking it's easier and simpler to save
    the items marked as user selected and re-add them once the removal sweep is
    complete.
    """
    def mark(self, opath):
        usersel = {}
        removed = []

        it = self.get_iter_first()
        # The name of the item we're removing, so that we can use it to find
        # other items which either depend on it, or were brought in by it
        marked_name = self[opath][self.COL_NAME]

        # Remove the passed item
        self.remove_item_path(opath)

        # Remove all dependent packages, update binb
        while it:
            path = self.get_path(it)
            it = self.iter_next(it)

            inc = self[path][self.COL_INC]
            deps = self[path][self.COL_DEPS]
            binb = self[path][self.COL_BINB].split(', ')
            itype = self[path][self.COL_TYPE]
            itname = self[path][self.COL_NAME]

            # We ignore anything that isn't a package
            if not itype == "package":
                continue

            # If the user added this item and it's not the item we're removing
            # we should keep it and its dependencies, the easiest way to do so
            # is to save its name and re-mark it for inclusion once dependency
            # processing is complete
            if "User Selected" in binb:
                usersel[itname] = self[path][self.COL_IMG]

            # If the iterated item is included and depends on the removed
            # item it should also be removed.
            # FIXME: need to ensure partial name matching doesn't happen
            if inc and marked_name in deps and itname not in removed:
                # found a dependency, remove it
                removed.append(itname)
                self.mark(path)

            # If the iterated item was brought in by the removed (passed) item
            # try and find an alternative dependee and update the binb column
            if inc and marked_name in binb:
                binb.remove(marked_name)
                self[path][self.COL_BINB] = ', '.join(binb).lstrip(', ')

        # Re-add any removed user selected items
        for u in usersel:
            npath = self.find_path_for_item(u)
            self.include_item(item_path=npath,
                              binb="User Selected",
                              image_contents=usersel[u])
    """
    Remove items from contents if the have an empty COL_BINB (brought in by)
    caused by all packages they are a dependency of being removed.
    If the item isn't a package we leave it included.
    """
    def sweep_up(self):
        it = self.contents.get_iter_first()
        while it:
            binb = self.contents.get_value(it, self.COL_BINB)
            itype = self.contents.get_value(it, self.COL_TYPE)
            remove = False

            if itype == 'package' and not binb:
                oit = self.contents.convert_iter_to_child_iter(it)
                opath = self.get_path(oit)
                self.mark(opath)
                remove = True

            # When we remove a package from the contents model we alter the
            # model, so continuing to iterate is bad. *Furthermore* it's
            # likely that the removal has affected an already iterated item
            # so we should start from the beginning anyway.
            # Only when we've managed to iterate the entire contents model
            # without removing any items do we allow the loop to exit.
            if remove:
                it = self.contents.get_iter_first()
            else:
                it = self.contents.iter_next(it)

    """
    Check whether the item at item_path is included or not
    """
    def contents_includes_path(self, item_path):
        return self[item_path][self.COL_INC]

    """
    Add this item, and any of its dependencies, to the image contents
    """
    def include_item(self, item_path, binb="", image_contents=False):
        item_name = self[item_path][self.COL_NAME]
        item_deps = self[item_path][self.COL_DEPS]

        self[item_path][self.COL_INC] = True

        item_bin = self[item_path][self.COL_BINB].split(', ')
        if binb and not binb in item_bin:
            item_bin.append(binb)
            self[item_path][self.COL_BINB] = ', '.join(item_bin).lstrip(', ')

        # We want to do some magic with things which are brought in by the
        # base image so tag them as so
        if image_contents:
            self[item_path][self.COL_IMG] = True
            if self[item_path][self.COL_TYPE] == 'image':
                self.selected_image = item_name

        if item_deps:
            # Ensure all of the items deps are included and, where appropriate,
            # add this item to their COL_BINB
            for dep in item_deps.split(" "):
                # If the contents model doesn't already contain dep, add it
                dep_path = self.find_path_for_item(dep)
                if not dep_path:
                    continue
                dep_included = self.contents_includes_path(dep_path)

                if dep_included and not dep in item_bin:
                    # don't set the COL_BINB to this item if the target is an
                    # item in our own COL_BINB
                    dep_bin = self[dep_path][self.COL_BINB].split(', ')
                    if not item_name in dep_bin:
                        dep_bin.append(item_name)
                        self[dep_path][self.COL_BINB] = ', '.join(dep_bin).lstrip(', ')
                elif not dep_included:
                    self.include_item(dep_path, binb=item_name, image_contents=image_contents)

    """
    Find the model path for the item_name
    Returns the path in the model or None
    """
    def find_path_for_item(self, item_name):
        # We don't include virtual/* or *-native items in the model so save a
        # heavy iteration loop by exiting early for these items
        if self.non_target_name(item_name):
            return None

        it = self.get_iter_first()
        while it:
            if (self.get_value(it, self.COL_NAME) == item_name):
                return self.get_path(it)
            else:
                it = self.iter_next(it)
        return None

    """
    Empty self.contents by setting the include of each entry to None
    """
    def reset(self):
        # Deselect images - slightly more complex logic so that we don't
        # have to iterate all of the contents of the main model, instead
        # just iterate the images model.
        if self.selected_image:
            iit = self.images.get_iter_first()
            while iit:
                pit = self.images.convert_iter_to_child_iter(iit)
                self.set(pit, self.COL_INC, False)
                iit = self.images.iter_next(iit)
            self.selected_image = None

        it = self.contents.get_iter_first()
        while it:
            oit = self.contents.convert_iter_to_child_iter(it)
            self.set(oit,
                     self.COL_INC, False,
                     self.COL_BINB, "",
                     self.COL_IMG, False)
            # As we've just removed the first item...
            it = self.contents.get_iter_first()

    """
    Returns two lists. One of user selected packages and the other containing
    all selected packages
    """
    def get_selected_packages(self):
        allpkgs = []
        userpkgs = []

        it = self.contents.get_iter_first()
        while it:
            sel = "User Selected" in self.contents.get_value(it, self.COL_BINB)
            name = self.contents.get_value(it, self.COL_NAME)
            allpkgs.append(name)
            if sel:
                userpkgs.append(name)
            it = self.contents.iter_next(it)
        return userpkgs, allpkgs

    """
    Return a squished (uniquified) list of the PN's of all selected items
    """
    def get_selected_pn(self):
        pns = []

        it = self.contents.get_iter_first()
        while it:
            if self.contents.get_value(it, self.COL_BINB):
                pns.append(self.contents.get_value(it, self.COL_PN))
            it = self.contents.iter_next(it)

        return self.squish(pns)

    def image_contents_removed(self):
        it = self.get_iter_first()
        while it:
            sel = self.get_value(it, self.COL_INC)
            img = self.get_value(it, self.COL_IMG)
            if img and not sel:
                return True
            it = self.iter_next(it)
        return False

    def get_build_rep(self):
        userpkgs, allpkgs = self.get_selected_packages()
        # If base image contents have been removed start from an empty rootfs
        if not self.selected_image or self.image_contents_removed():
            image = "empty"
        else:
            image = self.selected_image

        return BuildRep(" ".join(userpkgs), " ".join(allpkgs), image)

    def find_reverse_depends(self, pn):
        revdeps = []
        it = self.contents.get_iter_first()

        while it:
            name = self.contents.get_value(it, self.COL_NAME)
            itype = self.contents.get_value(it, self.COL_TYPE)
            deps = self.contents.get_value(it, self.COL_DEPS)

            it = self.contents.iter_next(it)

            if not itype == 'package':
                continue

            if pn in deps:
                revdeps.append(name)

        if pn in revdeps:
            revdeps.remove(pn)
        return revdeps

    def set_selected_image(self, img):
        self.selected_image = img
        path = self.find_path_for_item(img)
        self.include_item(item_path=path,
                          binb="User Selected",
                          image_contents=True)

        self.emit("image-changed", self.selected_image)

    def set_selected_packages(self, pkglist):
        selected = pkglist
        it = self.get_iter_first()

        while it:
            name = self.get_value(it, self.COL_NAME)
            if name in pkglist:
                pkglist.remove(name)
                path = self.get_path(it)
                self.include_item(item_path=path,
                                  binb="User Selected")
                if len(pkglist) == 0:
                    return
            it = self.iter_next(it)

    def find_image_path(self, image):
        it = self.images.get_iter_first()

        while it:
            image_name = self.images.get_value(it, self.COL_NAME)
            if image_name == image:
                path = self.images.get_value(it, self.COL_PATH)
                meta_pattern = "(\S*)/(meta*/)(\S*)"
                meta_match = re.search(meta_pattern, path)
                if meta_match:
                    _, lyr, bbrel = path.partition(meta_match.group(2))
                    if bbrel:
                        path = bbrel
                return path
            it = self.images.iter_next(it)
