import json
from collections import OrderedDict
from gluon import URL, IS_SLUG
from functools import reduce

# compliant with https://github.com/collection-json/spec
# also compliant with http://code.ge/media-types/collection-next-json/

"""

Example controller:

def api():
    from gluon.contrib.hypermedia import Collection
    policies = {
        'thing': {
            'GET':{'query':None,'fields':['id', 'name']},
            'POST':{'query':None,'fields':['name']},
            'PUT':{'query':None,'fields':['name']},
            'DELETE':{'query':None},
            },
        'attr': {
            'GET':{'query':None,'fields':['id', 'name', 'thing']},
            'POST':{'query':None,'fields':['name', 'thing']},
            'PUT':{'query':None,'fields':['name', 'thing']},
            'DELETE':{'query':None},
            },
        }
    return Collection(db).process(request, response, policies)
"""

__all__ = ['Collection']

class Collection(object):

    VERSION = '1.0'
    MAXITEMS = 100

    def __init__(self,db, extensions=True, compact=False):
        self.db = db
        self.extensions = extensions
        self.compact = compact

    def row2data(self,table,row,text=False):
        """ converts a DAL Row object into a collection.item """
        data = []
        if self.compact:
            for fieldname in (self.table_policy.get('fields',table.fields)):
                field = table[fieldname]
                if not ((field.type=='text' and text==False) or
                        field.type=='blob' or
                        field.type.startswith('reference ') or
                        field.type.startswith('list:reference ')) and field.name in row:
                    data.append(row[field.name])
        else:
            for fieldname in (self.table_policy.get('fields',table.fields)):
                field = table[fieldname]
                if not ((field.type=='text' and text==False) or
                        field.type=='blob' or
                        field.type.startswith('reference ') or
                        field.type.startswith('list:reference ')) and field.name in row:
                    data.append({'name':field.name,'value':row[field.name],
                                 'prompt':field.label, 'type':field.type})
        return data

    def row2links(self,table,row):
        """ converts a DAL Row object into a set of links referencing the row """
        links = []
        for field in table._referenced_by:
            if field._tablename in self.policies:
                if row:
                    href = URL(args=field._tablename,vars={field.name:row.id},scheme=True)
                else:
                    href = URL(args=field._tablename,scheme=True)+'?%s={id}' % field.name
                links.append({'rel':'current','href':href,'prompt':str(field),
                              'type':'children'})
        if row:
            fields = self.table_policy.get('fields', table.fields)
            for fieldname in fields:
                field = table[fieldname]
                if field.type.startswith('reference '):
                    href = URL(args=field.type[10:],vars={'id':row[fieldname]},
                               scheme=True)
                    links.append({'rel':'current','href':href,'prompt':str(field),
                                  'type':'parent'})

            for fieldname in fields:
                field = table[fieldname]
                if field.type=='upload' and row[fieldname]:
                    href = URL('download',args=row[fieldname],scheme=True)
                    links.append({'rel':'current','href':href,'prompt':str(field),
                                  'type':'attachment'})

            # should this be supported?
            for rel,build in (self.table_policy.get('links',{}).items()):
                links.append({'rel':'current','href':build(row),'prompt':rel})
        # not sure
        return links

    def table2template(self,table):
        """ confeverts a table into its form template """
        data = []
        fields = self.table_policy.get('fields', table.fields)
        for fieldname in fields:
            field = table[fieldname]
            info = {'name': field.name, 'value': '', 'prompt': field.label}
            policies = self.policies[table._tablename]
            # https://github.com/collection-json/extensions/blob/master/template-validation.md
            info['type'] = str(field.type) # FIX THIS
            if hasattr(field,'regexp_validator'):
                info['regexp'] = field.regexp_validator
            info['required'] = field.required
            info['post_writable'] = field.name in policies['POST'].get('fields',fields)
            info['put_writable'] = field.name in policies['PUT'].get('fields',fields)
            info['options'] = {} # FIX THIS
            data.append(info)
        return {'data':data}

    def request2query(self,table,vars):
        """ parses a request and converts it into a query """
        if len(self.request.args)>1:
            vars.id = self.request.args[1]

        fieldnames = table.fields
        queries = [table]
        limitby = [0,self.MAXITEMS+1]
        orderby = 'id'
        for key,value in vars.items():
            if key=='_offset':
                limitby[0] = int(value) # MAY FAIL
            elif key == '_limit':
                limitby[1] = int(value)+1 # MAY FAIL
            elif key=='_orderby':
                orderby = value
            elif key in fieldnames:
                queries.append(table[key] == value)
            elif key.endswith('.eq') and key[:-3] in fieldnames: # for completeness (useless)
                queries.append(table[key[:-3]] == value)
            elif key.endswith('.lt') and key[:-3] in fieldnames:
                queries.append(table[key[:-3]] < value)
            elif key.endswith('.le') and key[:-3] in fieldnames:
                queries.append(table[key[:-3]] <= value)
            elif key.endswith('.gt') and key[:-3] in fieldnames:
                queries.append(table[key[:-3]] > value)
            elif key.endswith('.ge') and key[:-3] in fieldnames:
                queries.append(table[key[:-3]] >= value)
            elif key.endswith('.contains') and key[:-9] in fieldnames:
                queries.append(table[key[:-9]].contains(value))
            elif key.endswith('.startswith') and key[:-11] in fieldnames:
                queries.append(table[key[:-11]].startswith(value))
            elif key.endswith('.ne') and key[:-3] in fieldnames:
                queries.append(table[key][:-3] != value)
            else:
                raise ValueError("Invalid Query")
        filter_query = self.table_policy.get('query')
        if filter_query:
            queries.append(filter_query)
        query = reduce(lambda a,b:a&b,queries[1:]) if len(queries)>1 else queries[0]
        orderby = [table[f] if f[0]!='~' else ~table[f[1:]] for f in orderby.split(',')]
        return (query, limitby, orderby)

    def table2queries(self,table, href):
        """ generates a set of collection.queries examples for the table """
        data = []
        for fieldname in (self.table_policy.get('fields', table.fields)):
            data.append({'name':fieldname,'value':''})
            if self.extensions:
                data.append({'name':fieldname+'.ne','value':''}) # NEW !!!
                data.append({'name':fieldname+'.lt','value':''})
                data.append({'name':fieldname+'.le','value':''})
                data.append({'name':fieldname+'.gt','value':''})
                data.append({'name':fieldname+'.ge','value':''})
                if table[fieldname].type in ['string','text']:
                    data.append({'name':fieldname+'.contains','value':''})
                    data.append({'name':fieldname+'.startswith','value':''})
                data.append({'name':'_limitby','value':''})
                data.append({'name':'_offset','value':''})
                data.append({'name':'_orderby','value':''})
        return [{'rel' : 'search', 'href' : href, 'prompt' : 'Search', 'data' : data}]

    def process(self,request,response,policies=None):
        """ the main method, processes a request, filters by policies and produces a JSON response """
        self.request = request
        self.response = response
        self.policies = policies
        db = self.db
        tablename = request.args(0)
        r = OrderedDict()
        r['version'] = self.VERSION
        tablenames = policies.keys() if policies else db.tables
        # if there is no tables
        if not tablename:
            r['href'] = URL(scheme=True),
            # https://github.com/collection-json/extensions/blob/master/model.md
            r['links'] = [{'rel' : t, 'href' : URL(args=t,scheme=True), 'model':t}
                          for t in tablenames]
            response.headers['Content-Type'] = 'application/vnd.collection+json'
            return response.json({'collection':r})
        # or if the tablenames is invalid
        if not tablename in tablenames:
            return self.error(400,'BAD REQUEST','Invalid table name')
        # of if the method is invalid
        if not request.env.request_method in policies[tablename]:
            return self.error(400,'BAD REQUEST','Method not recognized')
        # get the policies
        self.table_policy = policies[tablename][request.env.request_method]
        # process GET
        if request.env.request_method=='GET':
            table = db[tablename]
            r['href'] = URL(args=tablename)
            r['items'] = items = []
            try:
                (query, limitby, orderby) = self.request2query(table,request.get_vars)
                fields = [table[fn] for fn in (self.table_policy.get('fields', table.fields))]
                fields = filter(lambda field: field.readable, fields)
                rows = db(query).select(*fields,**dict(limitby=limitby, orderby=orderby))
            except:
                db.rollback()
                return self.error(400,'BAD REQUEST','Invalid Query')
            r['items_found'] = db(query).count()
            delta = limitby[1]-limitby[0]-1
            r['links'] = self.row2links(table,None) if self.compact else []
            text = r['items_found']<2
            for row in rows[:delta]:
                id = row.id
                for name in ('slug','fullname','title','name'):
                    if name in row:
                        href = URL(args=(tablename,id),scheme=True)
                        break
                else:
                    href = URL(args=(tablename,id),scheme=True)
                if self.compact:
                    items.append(self.row2data(table,row,text))
                else:
                    items.append({
                            'href':href,
                            'data':self.row2data(table,row,text),
                            'links':self.row2links(table,row)
                            });
            if self.extensions and len(rows)>delta:
                vars = dict(request.get_vars)
                vars['_offset'] = limitby[1]-1
                vars['_limit'] = limitby[1]-1+delta
                r['next'] = {'rel':'next',
                             'href':URL(args=request.args,vars=vars,scheme=True)}
            if self.extensions and limitby[0]>0:
                vars = dict(request.get_vars)
                vars['_offset'] = max(0,limitby[0]-delta)
                vars['_limit'] = limitby[0]
                r['previous'] = {'rel':'previous',
                                 'href':URL(args=request.args,vars=vars,scheme=True)}
            data = []
            if not self.compact:
                r['queries'] = self.table2queries(table, r['href'])
            r['template'] = self.table2template(table)
            response.headers['Content-Type'] = 'application/vnd.collection+json'
            return response.json({'collection':r})
        # process DELETE
        elif request.env.request_method=='DELETE':
            table = db[tablename]
            if not request.get_vars:
                return self.error(400, "BAD REQUEST", "Nothing to delete")
            else:
                try:
                    (query, limitby, orderby) = self.request2query(table, request.vars)
                    n = db(query).delete() # MAY FAIL
                    response.status = 204
                    return ''
                except:
                    db.rollback()
                    return self.error(400,'BAD REQUEST','Invalid Query')
            return response.json(r)
        # process POST and PUT (on equal footing!)
        elif request.env.request_method in ('POST','PUT'): # we treat them the same!
            table = db[tablename]
            if 'json' in request.env.content_type:
                data = request.post_vars.data
            else:
                data = request.post_vars
            if request.get_vars or len(request.args)>1: # update
                # ADD validate fields and return error
                try:
                    (query, limitby, orderby) = self.request2query(table, request.get_vars)
                    fields = filter(lambda fn_value:table[fn_value[0]].writable,data.items())
                    res = db(query).validate_and_update(**dict(fields)) # MAY FAIL
                    if res.errors:
                        return self.error(400,'BAD REQUEST','Validation Error',res.errors)
                    else:
                        response.status = 200
                        return ''
                except:
                    db.rollback()
                    return self.error(400,'BAD REQUEST','Invalid Query')
            else: # create
                # ADD validate fields and return error
                try:
                    fields = filter(lambda fn_value1:table[fn_value1[0]].writable,data.items())
                    res = table.validate_and_insert(**dict(fields)) # MAY FAIL
                    if res.errors:
                        return self.error(400,'BAD REQUEST','Validation Error',res.errors)
                    else:
                        response.status = 201
                        response.headers['location'] = \
                            URL(args=(tablename,res.id),scheme=True)
                        return ''
                except SyntaxError as e: #Exception,e:
                    db.rollback()
                    return self.error(400,'BAD REQUEST','Invalid Query:'+e)

    def error(self,code="400", title="BAD REQUEST", message="UNKNOWN", form_errors={}):
        request, response = self.request, self.response
        r = OrderedDict({
                "version" : self.VERSION,
                "href" : URL(args=request.args,vars=request.vars),
                "error" : {
                    "title" : title,
                    "code" : code,
                    "message" : message}})
        if self.extensions and form_errors:
            # https://github.com/collection-json/extensions/blob/master/errors.md
            r['errors'] = errors = {}
            for key, value in form_errors.items():
                errors[key] = {'title':'Validation Error','code':'','message':value}
                response.headers['Content-Type'] = 'application/vnd.collection+json'
        response.status = 400
        return response.json({'collection':r})

example_policies = {
    'thing': {
        'GET':{'query':None,'fields':['id', 'name']},
        'POST':{'query':None,'fields':['name']},
        'PUT':{'query':None,'fields':['name']},
        'DELETE':{'query':None},
        },
    'attr': {
        'GET':{'query':None,'fields':['id', 'name', 'thing']},
        'POST':{'query':None,'fields':['name', 'thing']},
        'PUT':{'query':None,'fields':['name', 'thing']},
        'DELETE':{'query':None},
        },
    }
