# -*- coding: utf-8 -*-

""" S3 Hierarchy Toolkit

    @copyright: 2013-14 (c) Sahana Software Foundation
    @license: MIT

    @requires: U{B{I{gluon}} <http://web2py.com>}

    Permission is hereby granted, free of charge, to any person
    obtaining a copy of this software and associated documentation
    files (the "Software"), to deal in the Software without
    restriction, including without limitation the rights to use,
    copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following
    conditions:

    The above copyright notice and this permission notice shall be
    included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    OTHER DEALINGS IN THE SOFTWARE.

    @status: experimental
"""

__all__ = ("S3Hierarchy",)

try:
    import json # try stdlib (Python 2.6)
except ImportError:
    try:
        import simplejson as json # try external module
    except:
        import gluon.contrib.simplejson as json # fallback to pure-Python module

from gluon import *
from s3utils import s3_unicode

DEFAULT = lambda: None

# =============================================================================
class S3Hierarchy(object):
    """ Class representing an object hierarchy """

    # -------------------------------------------------------------------------
    def __init__(self,
                 tablename=None,
                 hierarchy=None,
                 represent=None,
                 filter=None,
                 leafonly=True,
                 ):
        """
            Constructor

            @param tablename: the tablename
            @param hierarchy: the hierarchy setting for the table
                              (replaces the current setting)
            @param represent: a representation method for the node IDs
            @param filter: additional filter query for the table to
                           select the relevant subset
            @param leafonly: filter strictly for leaf nodes
        """

        self.tablename = tablename
        if hierarchy:
            current.s3db.configure(tablename, hierarchy=hierarchy)
        self.represent = represent

        self.filter = filter
        self.leafonly = leafonly

        self.__theset = None
        self.__flags = None

        self.__nodes = None
        self.__roots = None

        self.__pkey = None
        self.__fkey = None
        self.__ckey = None

        self.__left = DEFAULT

    # -------------------------------------------------------------------------
    @property
    def theset(self):
        """
            The raw nodes dict like:

                {<node_id>: {"p": <parent_id>,
                             "c": <category>,
                             "s": set(child nodes)
                }}
        """

        if self.__theset is None:
            self.__connect()
        if self.__status("dirty"):
            self.read()
        return self.__theset

    # -------------------------------------------------------------------------
    @property
    def flags(self):
        """ Dict of status flags """

        if self.__flags is None:
            theset = self.theset
        return self.__flags

    # -------------------------------------------------------------------------
    @property
    def config(self):
        """ Hierarchy configuration of the target table """

        tablename = self.tablename
        if tablename:
            s3db = current.s3db
            if tablename in current.db or \
               s3db.table(tablename, db_only=True):
                return s3db.get_config(tablename, "hierarchy")
        return None

    # -------------------------------------------------------------------------
    @property
    def nodes(self):
        """ The nodes in the subset """

        theset = self.theset
        if self.__nodes is None:
            self.__subset()
        return self.__nodes

    # -------------------------------------------------------------------------
    @property
    def roots(self):
        """ The IDs of the root nodes in the subset """

        nodes = self.nodes
        return self.__roots
        
    # -------------------------------------------------------------------------
    @property
    def pkey(self):
        """ The parent key """

        if self.__pkey is None:
            self.__keys()
        return self.__pkey

    # -------------------------------------------------------------------------
    @property
    def fkey(self):
        """ The foreign key referencing the parent key """

        if self.__fkey is None:
            self.__keys()
        return self.__fkey

    # -------------------------------------------------------------------------
    @property
    def left(self):
        """ The left join with the link table containing the foreign key """

        if self.__left is DEFAULT:
            self.__keys()
        return self.__left
        
    # -------------------------------------------------------------------------
    @property
    def ckey(self):
        """ The category field """

        if self.__ckey is None:
            self.__keys()
        return self.__ckey

    # -------------------------------------------------------------------------
    def __connect(self):
        """ Connect this instance to the hierarchy """

        tablename = self.tablename
        if tablename :
            hierarchies = current.model.hierarchies
            if tablename in hierarchies:
                hierarchy = hierarchies[tablename]
                self.__theset = hierarchy["nodes"]
                self.__flags = hierarchy["flags"]
            else:
                self.__theset = dict()
                self.__flags = dict()
                self.load()
                hierarchy = {"nodes": self.__theset,
                             "flags": self.__flags}
                hierarchies[tablename] = hierarchy
        else:
            self.__theset = dict()
            self.__flags = dict()
        return

    # -------------------------------------------------------------------------
    def __status(self, flag=None, default=None, **attr):
        """
            Check or update status flags

            @param flag: the name of the status flag to return
            @param default: the default value if the flag is not set
            @param attr: key-value pairs for flags to set

            @return: the value of the requested flag, or all flags
                     as dict if no flag was specified
        """

        flags = self.flags
        for k, v in attr.items():
            if v is not None:
                flags[k] = v
            else:
                flags.pop(k, None)
        if flag is not None:
            return flags.get(flag, default)
        return flags

    # -------------------------------------------------------------------------
    def load(self):
        """ Try loading the hierarchy from s3_hierarchy """

        if not self.config:
            return
        tablename = self.tablename

        if not self.__status("dbstatus", True):
            # Cancel attempt if DB is known to be dirty
            self.__status(dirty=True)
            return

        htable = current.s3db.s3_hierarchy
        query = (htable.tablename == tablename)
        row = current.db(query).select(htable.dirty,
                                       htable.hierarchy,
                                       limitby=(0, 1)).first()
        if row and not row.dirty:
            data = row.hierarchy
            theset = self.__theset
            theset.clear()
            for node_id, item in data["nodes"].items():
                theset[long(node_id)] = {"p": item["p"],
                                         "c": item["c"],
                                         "s": set(item["s"]) \
                                              if item["s"] else set()}
            self.__status(dirty=False,
                          dbupdate=None,
                          dbstatus=True)
            return
        else:
            self.__status(dirty=True,
                          dbupdate=None,
                          dbstatus=False if row else None)
        return

    # -------------------------------------------------------------------------
    def save(self):
        """ Save this hierarchy in s3_hierarchy """

        if not self.config:
            return
        tablename = self.tablename

        theset = self.theset
        if not self.__status("dbupdate"):
            return
            
        # Serialize the theset
        nodes_dict = dict()
        for node_id, node in theset.items():
            nodes_dict[node_id] = {"p": node["p"],
                                   "c": node["c"],
                                   "s": list(node["s"]) \
                                        if node["s"] else []}

        # Generate record
        data = {"tablename": tablename,
                "dirty": False,
                "hierarchy": {"nodes": nodes_dict}
                }

        # Get current entry
        htable = current.s3db.s3_hierarchy
        query = (htable.tablename == tablename)
        row = current.db(query).select(htable.id,
                                       limitby=(0, 1)).first()

        if row:
            # Update record
            row.update_record(**data)
        else:
            # Create new record
            htable.insert(**data)

        # Update status
        self.__status(dirty=False, dbupdate=None, dbstatus=True)
        return
        
    # -------------------------------------------------------------------------
    @classmethod
    def dirty(cls, tablename):
        """
            Mark this hierarchy as dirty. To be called when the target
            table gets updated (can be called repeatedly).

            @param tablename: the tablename
        """

        s3db = current.s3db

        if not tablename:
            return
        config = s3db.get_config(tablename, "hierarchy")
        if not config:
            return
        
        hierarchies = current.model.hierarchies
        if tablename in hierarchies:
            hierarchy = hierarchies[tablename]
            flags = hierarchy["flags"]
        else:
            flags = {}
            hierarchies[tablename] = {"nodes": dict(),
                                      "flags": flags}
        flags["dirty"] = True

        dbstatus = flags.get("dbstatus", True)
        if dbstatus:
            htable = current.s3db.s3_hierarchy
            query = (htable.tablename == tablename)
            row = current.db(query).select(htable.id,
                                           htable.dirty,
                                           limitby=(0, 1)).first()
            if not row:
                htable.insert(tablename=tablename, dirty=True)
            elif not row.dirty:
                row.update_record(dirty=True)
            flags["dbstatus"] = False
        return

    # -------------------------------------------------------------------------
    def read(self):
        """ Rebuild this hierarchy from the target table """

        tablename = self.tablename
        if not tablename:
            return

        s3db = current.s3db
        table = s3db[tablename]

        pkey = self.pkey
        fkey = self.fkey
        ckey = self.ckey

        fields = [pkey, fkey]
        if ckey is not None: