# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# Copyright (c) 2004 DoCoMo Euro-Labs GmbH (Munich, Germany).
# http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""a RedLand (http://librdf.org/) based knowledge database

:version: $Revision:$  
:author: Logilab

:copyright:
  2004 LOGILAB S.A. (Paris, FRANCE)
  
  2004 DoCoMo Euro-Labs GmbH (Munich, Germany)
  
:contact:
  http://www.logilab.fr/ -- mailto:contact@logilab.fr
  
  http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com
"""

__revision__ = '$Id$'

# FIXME
# from PyLogDB import KBError
class KBError(Exception): pass 

from cStringIO import StringIO
import traceback
import re
import os

import RDF

from narval.public import AL_NS, url_to_file
from narval.elements.rdf import RDFStatementElement, RDQLResultLineElement
from narval.interfaces.rdf import IRDFStatement, IRDQLQuery
from narval.interfaces.base import ICommand
from narval.elements import create_error, create_command
from narval.elements.base import DataElement

######################################################################
DEFAULT_REDLAND_STORE_FILE_URL = 'file:$NARVAL_HOME/data/rdfstore.rdf'

MINIMAL_CONTENT = '''<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" />
'''

def redland_kb_file(obj=None):
    """return the path to the redland knowledge file

    :param obj: object adaptable to IURL

    :rtype: tuple(str, str)
    """
    path, encoding = url_to_file(obj, DEFAULT_REDLAND_STORE_FILE_URL)
    return path

class RedLandModel(RDF.Model):
    """convenience class initialized with a simple kb uri"""
    def __init__(self, kb_uri):
        self._filename = redland_kb_file(kb_uri)
        try:
            storage = RDF.FileStorage(self._filename)
        # XXX FIXME: wait for Dave Beckett feedback about unifying
        #            RDF.RedlandError and Redland_python.Error
        except:
            # Naive way one of the possible errors : self._filename
            # doesn't exist
            if not os.path.exists(self._filename):
                log(LOG_DEBUG, "%r doesn't exist, I'll create a default empty one" %
                    (self._filename))
                fp = file(self._filename, 'w')
                fp.write(MINIMAL_CONTENT)
                fp.close()
            else:
                # For now, we don't know how to hanlde other errors
                raise
        RDF.Model.__init__(self, storage)
        

    def save(self):
        """uses RDF.Serializer to serialize current model"""
        RDF.Serializer().serialize_model_to_file(self._filename, self)

    def exec_query(self, query, namespaces = {}):
        """execute query against current model"""
        # FIXME: break if query defines different NS for existing alias
        for alias, url in namespaces.items():
            #query = query.replace('<%s:' % alias, '<%s'%url)
            query = re.sub('%s:(\w+)' % alias, r'<%s\1>' % url, query)
        log(LOG_DEBUG, "Query = %s" % query)
        query = RDF.Query(query)
        if query is None :
            raise Exception("Bad Query")
        # execute() returns an iterator, and we need a list
        # result = [rlnode_as_string(elt) for elt in query.execute(self)]
        result = query.execute(self)
        if result is None :
            result = []
        else :
            result = list(result)
        log(LOG_DEBUG, "RESULT = %s" % result)
        return result

    
def element_as_rlnode(element):
    """converts a simple 'full text' element into a RedLand Node"""
    element = str(element)
    if element.startswith('http://'):
        return RDF.Uri(element)
    return RDF.Node(element)

def rlnode_as_string(rl_node):
    """converts a RDF.Node into a raw string"""
    if rl_node.is_resource():
        return str(rl_node.uri)
    elif rl_node.is_blank():
        return str(rl_node.blank_identifier)
    elif rl_node.is_literal():
        return rl_node.literal_value['string']
    return None

def rl2al_statement(rl_statement):
    """converts a RedLandStatement into a (Al)RDFStatement"""
    return RDFStatementElement(subject = rlnode_as_string(rl_statement.subject),
                               predicate = rlnode_as_string(rl_statement.predicate),
                               object = rlnode_as_string(rl_statement.object))

def al2rl_statement(al_statement):
    """concerts a (Al)RDFStatement into a RedLandStatement"""
    subj = al_statement.subject and element_as_rlnode(al_statement.subject)
    pred = element_as_rlnode(al_statement.predicate)
    obj = al_statement.object and element_as_rlnode(al_statement.object)
    return RDF.Statement(subj, pred, obj)


def unify_foaf(model):
    """
    XXX move to extension. Useful outside this module.
    """
    # search duplicate names
    names = {}
    query = RDF.Query('SELECT ?x, ?n WHERE (?x <http://xmlns.com/foaf/0.1/name> ?n)')
    results = query.execute(model)
    for res in results:
        names.setdefault(str(res['n']), []).append(res['x'])

    remap_id = {}
    for name, ids in names.items() :
        remap_id[name] = ids[0].blank_identifier

    # remap duplicates
    for name, ids in names.items():
        for nodeid in ids:
            if nodeid.blank_identifier == remap_id[name] :
                continue
            blk = RDF.Node(blank=nodeid.blank_identifier)

            # remap subject
            to_suppress = []
            search_stmt = RDF.Statement(blk, None, None)
            for stmt in model.find_statements(search_stmt):
                object = RDF.Node(stmt.object)
                if RDF.node_type_name(stmt.object.type) == 'NODE_TYPE_BLANK' :
                    for other_name, other_ids in names.items() :
                        other_ids = [other_id.blank_identifier for other_id in other_ids]
                        if stmt.object.blank_identifier in other_ids :
                            object = RDF.Node(blank=remap_id[other_name])
                new_stmt = RDF.Statement(RDF.Node(blank=remap_id[name]),
                                         RDF.Node(stmt.predicate),
                                         object)
                model.add_statement(new_stmt)
                to_suppress.append(stmt)

            # remove statements
            for stmt in to_suppress:
                model.remove_statement(stmt)
  
            # remap object
            to_suppress = []
            search_stmt = RDF.Statement(None, None, blk)
            for stmt in model.find_statements(search_stmt):
                subject = RDF.Node(stmt.subject)
                if RDF.node_type_name(stmt.subject.type) == 'NODE_TYPE_BLANK' :
                    for other_name, other_ids in names.items() :
                        other_ids = [other_id.blank_identifier for other_id in other_ids]
                        if stmt.subject.blank_identifier in other_ids :
                            subject = RDF.Node(blank=remap_id[other_name])
                new_stmt = RDF.Statement(subject,
                                         RDF.Node(stmt.predicate),
                                         RDF.Node(blank=remap_id[name]))
                model.add_statement(stmt)
                to_suppress.append(stmt)

            # remove statements
            for stmt in to_suppress:
                model.remove_statement(stmt)


# actions definitions start here ##############################################

MOD_XML = '''<?xml version="1.0" encoding="ISO-8859-1"?>
<module name="RedLandDB" xmlns:al='%s'>''' % AL_NS

def act_unify(inputs):
    """search for matching statements in the knowledge base"""
    print 'using backend',inputs['kb']
    model = RedLandModel(inputs['kb'])
    result = []
    for stmt in inputs['stmts']:
        for rl_stmt in model.find_statements(al2rl_statement(IRDFStatement(stmt))):
            result.append(rl2al_statement(rl_stmt))
    return {'stmts' : result}

MOD_XML += """
<al:action name='search-stmts' func='act_unify'>
  <al:description lang='en'>%s</al:description>

  <al:input id='stmts' use='yes' list='yes'>
    <al:match>IRDFStatement(elmt)</al:match>
  </al:input>
  <al:input id='kb' optional='yes'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:redland'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>

  <al:output id='stmts' list='yes'>
    <al:match>IRDFStatement(elmt)</al:match>
  </al:output>
</al:action>""" % act_unify.__doc__


def act_add_statements(inputs):
    """add a list of `IRDFStatement` elements to the knowledge base"""
    print 'using backend',inputs['kb']
    model = RedLandModel(inputs['kb'])
    for stmt in inputs['stmts']:
        statement = al2rl_statement(IRDFStatement(stmt))
        if statement is None:
            raise KBError("new RDF.Statement failed")
        print 'adding', statement
        model.add_statement(statement)
        print 'added'
##     try:
##          model.save()
##     except Exception, ex:
##         print 'error while saving model', ex
    return {}

MOD_XML += """
<al:action name='add-stmts' func='act_add_statements'>
  <al:description lang='en'>%s</al:description>

  <al:input id='stmts' use='yes' list='yes'>
    <al:match>IRDFStatement(elmt)</al:match>
  </al:input>
  <al:input id='kb' optional='yes'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:redland'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>
</al:action>""" % act_add_statements.__doc__


def act_export(inputs):
    """export all statements in the knowledge base"""
    model = RedLandModel(inputs['kb'])
    return {'stmts': [rl2al_statement(stmt)
                      for stmt in model.find_statements(RDF.Statement())]}
        
MOD_XML = MOD_XML + """
<al:action name='export-stmts' func='act_export'>
  <al:description lang='en'>%s</al:description>

  <al:input id='kb' optional='yes'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:redland'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>

  <al:output id='stmts' list='yes'>
    <al:match>IRDFStatement(elmt)</al:match>
  </al:output>
</al:action>""" % act_export.__doc__


def act_import_url(inputs):
    """import a FOAF file fetched from given URL"""
    log(LOG_DEBUG, "INPUTS %s" % inputs)
    model = RedLandModel(inputs['kb'])
    # XXX FIXME : RDF.Uri() doesn't accept unicode strings
    uri = RDF.Uri(str(ICommand(inputs['import-command']).args[1]))
    parser = RDF.Parser('raptor')
    if parser is None:
        raise Exception("Failed to create RDF.Parser raptor")
    try:
        parser.parse_into_model(model, uri, 'http://www.logilab.org/default.rdf')
    except Exception, exc:
        error = DataElement('Sorry, import failed\nlog: %s' % exc)
        return {'rdql-error' : error}
    unify_foaf(model)
    res = DataElement('imported %s' % uri)
    return {'rdql-info': res }

MOD_XML += """
<al:action name='import-url' func='act_import_url'>
  <al:description lang='en'>%s</al:description>

  <al:input id='import-command' use='yes'>
    <al:match>ICommand(elmt)</al:match>
  </al:input>
  <al:input id='kb' optional='yes'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:redland'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>

  <al:output id='rdql-error' optional='yes'>
    <al:match>isinstance(elmt, DataElement)</al:match>
  </al:output>
  <al:output id='rdql-info' optional='yes'>
    <al:match>IData(elmt)</al:match>
  </al:output>
</al:action>""" % act_import_url.__doc__


def act_import_string(inputs):
    """import a FOAF string: FIXME"""
    log(LOG_DEBUG, "INPUTS %s" % inputs)
    model = RedLandModel(inputs['kb'])
    rdf_string = ICommand(inputs['command']).args[1]
    parser = RDF.Parser('raptor')
    if parser is None:
        raise Exception("Failed to create RDF.Parser raptor")
    parser.parse_string_into_model(model, rdf_string, 'http://www.logilab.org/default.rdf')
    unify_foaf(model)
    res = DataElement('imported')
    return {'rdql-info': res }

MOD_XML += """
<al:action name='import-string' func='act_import_string'>
  <al:description lang='en'>%s</al:description>

  <al:input id='command' use='yes'>
    <al:match>ICommand(elmt)</al:match>
  </al:input>
  <al:input id='kb' optional='yes'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:redland'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>

  <al:output id='rdql-info'>
    <al:match>IData(elmt)</al:match>
  </al:output>
</al:action>""" % act_import_url.__doc__


def act_query_base(inputs):
    """queries the RDF knowledge base using RDQL"""
    namespaces = dict([(e.alias, e.address) for e in inputs['namespaces']])
    model = RedLandModel(inputs['kb'])
    query = IRDQLQuery(inputs['query']).query
    error = None
    query_results = None
    try:
        query_results = [RDQLResultLineElement(res) for res in
                         model.exec_query(query, namespaces)]
    except Exception, exc:
        data = StringIO()
        data.write('An error occured :\n')
        traceback.print_exc(file = data)
        error = DataElement(data.getvalue())
    if not error and not query_results:
        error = DataElement('Sorry, no result found')
    if error:
        return {'rdql-error' : error}
    return {'rdql-results' : query_results}


MOD_XML += """
<al:action name="rdql-query" func="act_query_base">
  <al:description lang='en'>%s</al:description>

  <al:input id='namespaces' optional='yes' list='yes'>
    <al:match>IRDFNamespace(elmt)</al:match>
  </al:input>
  <al:input id='query'>
    <al:match>IRDQLQuery(elmt)</al:match>
  </al:input>
  <al:input id='kb' optional='yes'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:redland'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>

  <al:output id='rdql-error' optional='yes'>
    <al:match>isinstance(elmt, DataElement)</al:match>
  </al:output>

  <al:output id='rdql-results' optional='yes' list='yes'>
    <al:match>IRDQLResultLine(elmt)</al:match>
  </al:output>
</al:action>""" % act_query_base.__doc__

MOD_XML +=  "</module>"


# http://starship.python.net/~fdrake/fdrake.rdf
