"""

   This is an abstraction somewhere between an object-oriented class
   hierarchy (as one might implement in Java) and an ontology language
   like OWL.  Hopefully it's close enough to both.  We're also
   interested in object serialization, so an object model is also kind
   of like a grammar.

   Some basic design points:

   * Classes and properties exist outside of any object model.  They
     can be used in zero or more object models.  Object models know
     about classes and properties, but classes and properties don't
     know about object models.

   * When a particular class uses a particular property, we have a
     PropertyUse.  The constraints on values are for a particular
     PropertyUse, which may inherit them from its property.  The ones
     on the Property are as in RDFS; the ones on the PropertyUse are
     as in OWL's Restrictions.

   * An object model is a set of Classes and PropertyUses, not just a
     set of classes.  So a particular class might appear to have 5
     properties in one object model and 9 in another, when it really
     has 12.  (Object models are closed worlds -- more properties &
     classes are out there happening all the time...)

   * Classes and properties have URIs, which can be considered to have
     a prefix (namespace) part and a local part.  namespace.py helps
     with this.  The collection of classes and properties into
     namespaces is INDEPENDENT of their being collected into object
     models.  Users may want to align these, of course, making the
     items with one namespace be exactly those items in the object
     model.

Issues:

   * It might be nice to say a model is exactly those
     Classes/Properties in a given namespace.  I guess we can just do
     this in the Extract-From-OWL.   
           model.addFromOWL(Graph, Id)
           model.addAllFromOWL(Graph[, Prefix])


TODO:
   - don't handle any URIs in here -- that's gotten horribly
     messy.    have a bi-map (uri_object_map.umap ?) or something.
     If Class and Property want to have a uri property, that's
     maybe okay...
     
   
****************************************************************

it'd be nice to use namespace to get short-names again....


from
   OWL
   asn06

to
   OWL
   asn06

create a template for to/from relaxng, bnf, ...?



Output Functions:

   >> print m.in_asn06()

Input Functions:

   >> m.web_load()      # from original URI

   objectmodel.load(URI)       (import?)    [ relative, file-based? ]
   objectmodel.create(URI)

"""
from namespace import ns

import qname

class Error(RuntimeError):
   pass

class ObjectModel:
    """
    >>> from objectmodel import ObjectModel, Class, Property
    >>> m = ObjectModel()

    >>> from namespace import Namespace, Resource
    >>> my = Namespace("http://example.com/my_stuff/", strict=False)

    >>> print m
        <no classes>
        <no floating properties>
    
    >>> m.add(Class(my.Animal))
    >>> print m
        class http://example.com/my_stuff/Animal
        <no floating properties>

    >>> m.add(Class(my.Mammal, [my.Animal]))

    >>> m.add(Class(my.Agent))
    >>> m.add(Class(my.Person, [my.Mammal, my.Agent]))
    >>> print m
        class http://example.com/my_stuff/Animal
        class http://example.com/my_stuff/Mammal
             superclass http://example.com/my_stuff/Animal
        class http://example.com/my_stuff/Agent
        class http://example.com/my_stuff/Person
             superclass http://example.com/my_stuff/Mammal
             superclass http://example.com/my_stuff/Agent
        <no floating properties>

    >>> m.add(Property(my.father, domain=my.Animal, to=my.Animal))
    

    """
    
    def __init__(self):
        self.properties = []
        self.classes = []

    def add(self, item):
        # check for dups?
        # maintain various indexes?
        #    make them be immutable so we can put them in a set?
        if isinstance(item, Class):
            self.classes.append(item)
        elif isinstance(item, Property):
            self.properties.append(item)
        else:
            raise Error, "you can only add instances of Class or Property"

    def elements(self) :
       for x in self.classes:
          yield x
       for x in self.properties:
          yield x
          
    def propertiesForClass(self, c) :
       for p in self.properties:
          if p in c.properties:
             yield(p)

    def propertiesForClassWithInheritance(self, c) :
       for sup in self.classes:
          if sup in c.superclasses:
             for p in self.propertiesForClassWithInheritance(sup):
                yield(p)
       for p in self.properties:
          if p in c.properties:
             yield(p)

    def count_pv(self, cls):
       """Return the number of propery-value pairs possible for
       instances of this class, None for indeterminate."""
       count = 0
       for p in self.propertiesForClassWithInheritance(cls):
          if p.list or p.multi or p.optional:
             return None
          count += 1
       return count
       
    def subclassesForClass(self, c) :
       for sub in self.classes:
          if c in sub.superclasses:
             yield(sub)

    def __str__(self):
        result = ""
        properties_done = []
        if self.classes:
            for c in self.classes:
                result += "    class " + str(c) + "\n"
                for p in c.properties:
                    if p in self.properties:
                        properties_done.append(p)
                        result += "         property " + str(p) + p.flags()+"\n"
                for super in c.superclasses:
                    result += "         superclass " + str(super) + "\n"
        else:
            result += "    <no classes>\n"
        floating_properties = []
        for p in self.properties:
            if p not in properties_done:
                floating_properties.append(p)
        if floating_properties:
            for p in floating_properties:
                result += "    floating property: " + str(p) + "\n"
        else:
            result += "    <no floating properties>\n"
        return result[0:-1]

    def namespaces_with_count(self, map):
        """Make sure all classes, properties, range classes, and
        superclasses are in a given qname.Map, and return a sorted
        list (count, shortname) pairs.  The 'count' only includes
        classs and properties directly in the model. 

        """
        count = { }
        for e in self.elements():
           (long, local) = qname.uri_split(uri(e))
           short = map.getShort(long, make=True)
           try:
              count[short] += 1
           except KeyError:
              count[short] = 1
        for p in self.properties:
           if p.to:
              (long, local) = qname.uri_split(uri(p.to))
              short = map.getShort(long, make=True)
              count.setdefault(short, 0)   # don't count these
        for c in self.classes:
           for superclass in c.superclasses:
              (long, local) = qname.uri_split(uri(superclass))
              short = map.getShort(long, make=True)
              count.setdefault(short, 0)   # don't count these
           
        counts = [ (value, key) for key, value in count.items() ]
        counts.sort()
        return counts

    def namespaces(self, map=None):
        """Calls namespaces_with_count and drops the counts.
        """
        if map is None:
           map = opendata.qname.Map()
        result = self.namespaces_with_count(self, map)
        return [ key for value, key in result ]

    def get_or_create_class(self, clsuri):
       for cls in self.classes:
          if cls.name == clsuri:
             return cls
       cls = Class(clsuri)
       self.add(cls)
       return cls

    def get_by_uri(self, uri):
       for item in self.elements():
          if item.name == uri:
             return item
       raise KeyError()

    def __getattr__(self, name):
       # rather a hack, eh?   should we use "the" namespace of the
       # model, instead?    At this point, I don't know whether
       # models have namespaces, or how that works.
       for item in self.elements():
          if (item.name.endswith("#"+name) or
              item.name.endswith("/"+name)) :
             return item
       raise AttributeError(name)
       
classes = {}
import sys
from namespace import Resource

def uri(resource) :
   if hasattr(resource, "uri"):
      resource = resource.uri
   resource = str(resource)
   return resource
   
class Class:
    """

    >>> from objectmodel import Class, Property
    >>> from namespace import Namespace, Resource
    >>> my = Namespace("http://example.com/my_stuff/", strict=False)
    
    >>> Class()
    objectmodel.Class(name='None', superclasses=[], properties=[])
    
    >>> c = Class(my.Animal)
    >>> c
    objectmodel.Class(name='http://example.com/my_stuff/Animal', superclasses=[], properties=[])

    >>> m = Class(my.Mammal)
    >>> m.addSuperclass(my.Animal)

    """

    def __init__(self, name=None, superclasses=None, properties=None):
        self.name = uri(name)
        classes[self.name] = self
        self.superclasses = []
        if superclasses:
           for super in superclasses:
              self.addSuperclass(uri(super))
        self.properties = properties or []

        #print >> sys.stderr, "Classes:", classes


    def addSuperclass(self, super):
       if isinstance(super, Class):
          self.superclasses.append(super)
          return

       superURI = uri(super)
       if superURI in classes.keys():
          super = classes[superURI]
          self.superclasses.append(super)
       else:
          print >> sys.stderr, "classes[%s] ?" % superURI
          print >> sys.stderr, "Classes:", classes
          print >> sys.stderr, "in?", (superURI in classes.keys())
          print >> sys.stderr, "keys?", classes.keys()
          print >> sys.stderr, "it", superURI
          print >> sys.stderr, "its class", superURI.__class__
          
          raise Error, ("You can't add a superclass until its URI is known (uri=%s)" % superURI)
       
    def __repr__(self):
        return ("objectmodel.Class(name=%s, superclasses=%s, properties=%s)" %
                ( repr(self.name),
                  self.superclasses,
                  self.properties
                  )
                )
    
    def __str__(self):
        return str(self.name)

    def writeTo(self, stream, prefix=".  "):
        stream.write(self.name)

    def sameOrSuper(self, other):
       """Is the other class the same as this one, or a superclass of
       this one? """ 
       if self == other:
          return true
       for s in self.superclasses:
          if s.hasSuperclass(other):
             return true

    
class Property:
    """

    >>> from objectmodel import Class, Property
    >>> from namespace import Namespace, Resource
    >>> my = Namespace("http://example.com/my_stuff/", strict=False)
    
    >>> Property()
    Property()

    >>> dummy = Class(my.Animal)
    >>> father = Property(my.father, domain=my.Animal, to=my.Animal, optional=False)

    xx>>> father
    Property(Resource("http://example.com/my_stuff/father"), domain=Resource("http://example.com/my_stuff/Animal"), to=Resource("http://example.com/my_stuff/Animal"), optional=False)


    """

    def __init__(self, name=None, domain=None, to=None, optional=None, multi=False, list=False):
        if domain:
           if isinstance(domain, Class):
              pass
           else: 
              domain = classes[uri(domain)]
           domain.properties.append(self)
        self.__dict__.update(locals())
        self.uri = str(name)

    def flags(self):
       result = ""
       if self.to:
          result += " to="+str(self.to)
       if self.multi:
          result += " multi"
       if self.list:
          result += " list"
       return result

    def __repr__(self):
        result = "Property("
        if self.name:
            result += repr(self.name)
        if self.domain:
            result += ", domain="+repr(self.domain)
        if self.to:
            result += ", to="+repr(self.to)
        if self.optional == False:
            result += ", optional=False"
        return result+")"
    
    def __str__(self):
        return str(self.name) + ":" + str(self.to)

def createTestModel1():
   """

   >>> from objectmodel import createTestModel1
   >>> tm = createTestModel1()
   
   """
   from namespace import Namespace, ns
   books = Namespace("http://example.com/books/", strict=False)
   om = ObjectModel()

   om.add(Class(books.Chainstore))
   om.add(Class(books.Store))
   om.add(Class(books.Bookstore, [books.Store]))
   om.add(Class(books.ChainBookstore, [books.Bookstore, books.Chainstore]))
   om.add(Class(books.SmallBookstore, [books.Bookstore]))
   om.add(Class(books.PrivateBookSeller, [books.SmallBookstore]))
   om.add(Class(books.Title))
   om.add(Class(books.Sale))
   om.add(Property(books.stockedTitles, domain=books.Bookstore, to=books.Title, multi=True))
   om.add(Property(books.titles, domain=books.Sale, to=books.Title, list=True))
   om.add(Property(books.amount, domain=books.Sale, to=ns.xsd.int))
   om.add(Property(books.date, domain=books.Sale, to=ns.xsd.string))

   return om


def createTestModel2():
   import asn0701
   p = asn0701.Parser()
   stream = open("test-data/animal-movies.asn", "r")
   return p.readModel(stream)


#asURI
#asObject(map)



if __name__ == "__main__":
    import doctest, sys
    doctest.testmod(sys.modules[__name__])
 
