#!/usr/bin/python

import inspect
import os
import pdb
import copy
import sys
import json
import re
import jinja2
from optparse import OptionParser
import lib

# Django set up

import django
sys.path.append('.')
sys.path.append('/opt/xos')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
from django.db.models.fields.related import ForeignKey, ManyToManyField
from django.contrib.contenttypes.fields import GenericRelation
from django.conf import settings
from django.contrib.contenttypes.models import ContentType

django.setup()

from core.models import PlCoreBase

options = None


def is_model_class(model):
    """ Return True if 'model' is something that we're interested in """
    if not inspect.isclass(model):
        return False
    if model.__name__ in ["PlModelMixIn"]:
        return False
    bases = inspect.getmro(model)
    bases = [x.__name__ for x in bases]
    if ("PlCoreBase" in bases) or ("PlModelMixIn" in bases):
        return True

    return False

def bases(model):
    return ','.join([b.__name__ for b in model.__bases__])

def module_has_models(module):
    """ return True if 'module' contains any models we're interested in """
    for k in dir(module):
        v=getattr(module,k)
        if is_model_class(v):
            return True

    return False

def app_get_models_module(app):
    """ check whether 'app' includes XOS models """

    app = app + ".models"
    try:
        models_module = __import__(app)
    except ImportError:
        return False

    for part in app.split(".")[1:]:
        if module_has_models(models_module):
            return models_module
        models_module = getattr(models_module,part)

    if module_has_models(models_module):
        return models_module

    return None


def singular(foo, keys):
	for k in keys:
		if (foo==k+'es'):
			return k
		elif (foo==k+'s'):
			return k
	raise Exception('Plural to singular error for %s'%foo)

g = globals()

def enum_classes(apps):
    global app_map
    global class_map
    app_map = {}
    class_map = {}
    model_classes = []
    for app in apps:
            orig_app=app
            models_module = app_get_models_module(app)

            for classname in dir(models_module):
                    c = getattr(models_module, classname, None)

                    # For services, prevent loading of core models as it causes
                    # duplication.
                    if hasattr(c,"_meta") and hasattr(c._meta, "app_label"):
                        if (c._meta.app_label == "core") and (orig_app!="core"):
                            continue

                    if is_model_class(c) and c.__name__ not in options.blacklist:
                            model_classes.append(c)
                            app_map[c.__name__]=orig_app
                            c.class_name = c.__name__
                            file_name = c.__module__.rsplit('.',1)[1]
                            try:
                                if (file_name not in class_map[orig_app]):
                                    class_map[orig_app].append({file_name:[c]})
                                else:
                                    class_map[orig_app][file_name].append(c)

                            except KeyError:
                                class_map[orig_app] = [{file_name:[c]}]


    return model_classes

class GenObj(object):
	def __str__(self):
		return str(self.model.__name__.lower())

	def __init__(self, m):
		self.model = m
		self.props = []
		self.fields = []
                self.all_fields = []
		self.field_dict = []
		self.refs = []
                self.reverse_refs = []
		self.plural_name = None
                self.content_type_id = ContentType.objects.get_for_model(m).id

	def plural(self):
		if (self.plural_name):
			return self.plural_name
		else:
			name = str(self)
			if (name.endswith('s')):
				return name+'es'
			else:
				return name+'s'

        def singular(self):
            return str(self)

        def rest_name(self):
            # These are things that either for historic reasons or due to incorrect naming,
            # got called something different than the autogen thinks they should be
            # called.
            REST_FIXUP = {'controllernetworkses': 'controllernetworks',
                            'controllerimageses': 'controllerimages',
                            'controllersliceses': 'controllerslices',
                            'controlleruserses': 'controllerusers',
                            'sitedeploymentses': 'sitedeployments',
                            'siteroles': 'site_roles',
                            'sliceprivileges': 'slice_privileges',
                            'sliceroles': 'slice_roles',
                            }
            return REST_FIXUP.get(self.plural(), self.plural())

	def camel(self):
		name = str(self.model.__name__)
		return name
		
class Generator(dict):
        def __init__(self):
            self.apps = {}

	def all(self):
		return self.values()

        def rest_models(self):
                norest = [x.lower() for x in options.norest]
                return [v for v in self.values() if not (str(v) in norest)]
	
	def regex(self, r):
		filtered = filter(lambda o:re.match(r,str(o)), self.values())
		return filtered

	def add_object(self, o):
                global app_map
		obj = GenObj(o)
		fields = o._meta.get_fields(include_hidden=False)
                try:
                    obj.app = app_map[o.__name__] # full name
                    if hasattr(o, "_meta") and hasattr(o._meta, "app_label"):
                        obj.app_name = o._meta.app_label
                    else:
                        obj.app_name = app_map[o.__name__].split(".")[-1]  # only the last part
                except KeyError:
                    print "KeyError: %r"%o.__name__

                obj.class_name = o.class_name
                obj.bases = bases(o)

                file_name = o.__module__.rsplit('.',1)[1]

                try:
                    if (file_name not in self.apps[obj.app]):
                        self.apps[obj.app][file_name]=[obj]
                    else:
                        self.apps[obj.app][file_name].append(obj)

                except KeyError:
                    self.apps[obj.app] = {file_name:[obj]}


		self[str(obj).lower()]=obj

	def compute_links(self):

		for obj in self.values():
                	base_props = [f.name for f in obj.model.__base__._meta.fields] + ['id']
			fields = list(obj.model._meta.fields + obj.model._meta.many_to_many)

                        other_fields = obj.model._meta.get_fields()
                        for o in other_fields:
                            if (type(o)==GenericRelation): fields+=[o]


			for f in fields:
                                if f.name in base_props or type(f).__name__=='ManyToOneRel': continue

				if (f and hasattr(f, 'rel') and f.rel):
					to_name = str(f.rel.to)
				else:
					to_name = None


                                if f.name.endswith("_ptr"):
                                        # django inherited model, for example HPCService
                                        # cause swagger and REST to break
                                        pass
				else:
                                        f.type = f.__class__.__name__

                                        if (type(f)==ForeignKey):
                                            f.related.model.class_name = f.related.model.__name__
                                        elif (type(f)==ManyToManyField or type(f)==GenericRelation):
                                            f.related.model.class_name = f.related.model.__name__
                                            try:
                                               f.related.model.through = f.related.through.__name__
                                            except AttributeError: 
                                               pass
                                
                
                                        if (f.name not in base_props):
                                            obj.fields.append(f)
                                        
                                        #if (f.name == 'tags'): pdb.set_trace()

                                        obj.all_fields.append(f)
                                        obj.props.append(f.name)

			m2m = obj.model._meta.many_to_many
			for f in m2m:
				try:
					related_model_name = f.m2m_reverse_field_name()
				except:
					related_model_name = f.m2m_db_table().rsplit('_',1)[-1]

				related_name = f.related_query_name()
				if related_model_name in self.keys():
                                        #print "XXX1", obj, f, related_name, related_model_name
					refobj = self[related_model_name]
					cobj = copy.deepcopy(obj)
					cobj.multi=True
					refobj.refs.append(cobj)

                                # deal with upgradeFrom_rel_+
                                if (related_name.endswith("+")):
                                    continue

				if (related_name!='+') and related_model_name in self: # and related_name.lower()!=str(obj).lower()):
                                        refobj = self[related_model_name]
                                        #print "XXX2", obj, f, related_name, related_model_name, refobj.plural_name
					cobj = copy.deepcopy(refobj)
					cobj.multi = True

					obj.refs.append(cobj)

                for obj in self.values():
                        # generate foreign key reverse references
                        for f in obj.model._meta.related_objects:
                            related_model = getattr(f, "related_model", None)
                            if not f.related_name:
                                continue
                            if "+" in f.related_name:
                                continue
                            if related_model and (related_model.__name__.lower() in self.keys()):
                                cobj = copy.deepcopy(self[related_model.__name__.lower()])
                                cobj.related_name = f.related_name
                                obj.reverse_refs.append(cobj)

def main():
        global options
        parser = OptionParser(usage="modelgen [options] <template_fn>", )

        parser.add_option("-d", "--dict", dest="dict",
             help="dictionary to replace text in output", metavar="DICT", default=[], action="append")

        parser.add_option("-a", "--app", dest="apps",
             help="list of applications to parse", metavar="APP", default=[], action="append")
        parser.add_option("-b", "--blacklist", dest="blacklist",
             help="add model name to blacklist", metavar="MODEL", default=["SingletonModel", "PlCoreBase"], action="append")
        parser.add_option("-n", "--no-rest", dest="norest",
             help="do not generate rest api for model", metavar="MODEL", default=["SingletonModel", "PlCoreBase"], action="append")
        parser.add_option("-l", "--local", dest="local",
             help="skip base models", metavar="MODEL", default=False, action="store_true")
        (options, args) = parser.parse_args(sys.argv[1:])

        template_name = os.path.abspath(args[0])

        # try to make sure we're running from the right place
        if (not os.path.exists("core")):
            if (os.path.exists("../core")):
                os.chdir("..")
            elif (os.path.exists("../../core")):
                os.chdir("../..")
            else:
                print >> sys.stderr, "Are you sure you're running modelgen from the root of an XOS installation"
                sys.exit(-1)

        if not options.apps:
            options.apps = ["core"]

        if options.apps == ["*"]:
            options.apps = [x for x in settings.INSTALLED_APPS if app_get_models_module(x)]

        if len(args)!=1:
            print 'Usage: modelgen [options] <template_fn>'
            exit(1)

	generator = Generator()

	models = enum_classes(options.apps)

	for m in models:
		generator.add_object(m)

	generator.compute_links()

        os_template_loader = jinja2.FileSystemLoader( searchpath=[os.path.split(template_name)[0]])
        os_template_env = jinja2.Environment(loader=os_template_loader)

        for x in dir(lib):
            if x.startswith('xp'):
                os_template_env.globals[x] = getattr(lib, x)

        template = os_template_env.get_template(os.path.split(template_name)[1])
        rendered = template.render({"generator": generator})

        lines = rendered.splitlines()
        current_buffer = []
        for l in lines:
            if (l.startswith('+++')):
                path = l[4:]
                
                direc,filename = path.rsplit('/',1)
                os.system('mkdir -p %s'%direc)
              
                fil = open(path,'w')
                buf = '\n'.join(current_buffer)

                obuf = buf
                for d in options.dict:
                    df = open(d).read()
                    d = json.loads(df)

                    pattern = re.compile(r'\b(' + '|'.join(d.keys()) + r')\b')
                    obuf = pattern.sub(lambda x: d[x.group()], buf)

                fil.write(obuf)
                fil.close()

                print 'Written to file %s'%path
                current_buffer = []
            else:
                current_buffer.append(l)
        if (current_buffer):
            print '\n'.join(current_buffer)


if (__name__=='__main__'):
	main()
