#!/usr/bin/env python
#coding: utf-8
#by yangbin at 2018.11.28
from __future__ import print_function
import os

from .conf import *
from .helper import *
from .objtpl import *

# Object Model to Code
'''
root object # 开始标记
topObjName TopObjName[] # comment, Top Object
    filedObjName [Prefix]filedObjName[Suffix] [*] # comment Filed Object
'''

class ObjNode(object):
    def __init__(self, lineno, line, fieldName, fieldType, containerType, refType, needDump, comment, parent):
        self.lineno = lineno
        self.line = line
        self.fieldName = fieldName
        self.fieldType = fieldType
        self.containerType = containerType
        self.refType = refType
        self.needDump = needDump
        self.comment = comment
        self.parent = parent
        self.children = []

    def addChildren(self, child):
        assert isinstance(child, ObjNode)
        self.children.append(child)
        return self

    def isRootObj(self):
        return self.lineno == 1

    def isBasicType(self):
        return self.containerType == OBJ_CONTAINER_TYPE_BASIC

    def isMgrType(self):
        return self.refType == OBJ_REF_TYPE_MANAGE
    
    def isRefType(self):
        return self.refType == OBJ_REF_TYPE_REF

    def isSingleType(self):
        return self.containerType == OBJ_CONTAINER_TYPE_SINGLE

    def isListType(self):
        return self.containerType == OBJ_CONTAINER_TYPE_LIST

    def longNamespace(self):
        if self.parent is None:
            return None
        if self.containerType == OBJ_CONTAINER_TYPE_BASIC:
            return None
        if self.refType == OBJ_REF_TYPE_REF:
            return None
        pn = self.parent.longNamespace()
        if pn is None:
            return self.fieldType[0].lower() + self.fieldType[1:]
        return pn[0].lower() + pn[1:] + self.fieldType

    def bigLongNamespace(self):
        return upperFirst(self.longNamespace())

    def shortNamespace(self):
        return self.fieldName

    def bigShortNamespace(self):
        return upperFirst(self.shortNamespace())

    def genObjFuncParam(self):
        pm = ''
        for child in self.children:
            if not child.isBasicType():
                continue
            pm += '%s %s,' % (child.fieldName, child.fieldType)
        return pm.strip(',')

    def show(self, ident):
        s = 4 * ident * ' '
        if self.lineno == 0:
            s = self.line
        fieldType = self.fieldType
        if self.containerType == OBJ_CONTAINER_TYPE_LIST:
            fieldType += '[]'
        if self.refType == OBJ_REF_TYPE_REF:
            fieldType = '@' + fieldType
        s += '%s %s %s # %s %s\n' % (self.fieldName, fieldType, self.needDump, self.longNamespace(), self.comment)
        for child in self.children:
            s += child.show(ident+1)
        return s
    # import and define
    def objCode0(self):
        code = '// Code generated by m2c. DO NOT EDIT.\npackage %s\n' % self.fieldName
        code += 'import (\n'
        code += '"time"\n'
        code += '"github.com/globalsign/mgo"\n'
        code += '"github.com/globalsign/mgo/bson"\n'
        code += 'id "%s/ak/orm/id"\n' % MOD
        for child in self.children:
            assert isinstance(child, ObjNode)
            if child.isBasicType(): # skip basic type
                continue
            if child.isRefType():
                code += '%sObject "%s"\n' % (child.shortNamespace(), child.objImportPath())
            else:
                code += '%sObject "%s"\n' % (child.longNamespace(), child.objImportPath())
        code += ')\n'

        code += 'var (\n'
        code += '%sTable *mgo.Collection\n' % self.longNamespace()
        code += 'db      *mgo.Database\n'
        code += ')\n'

        code += 'func Init(mgo *mgo.Database) {\n'
        code += 'db = mgo\n'
        code += '%sTable = mgo.C("%s")\n' % (self.longNamespace(), self.longNamespace())
        for child in self.children:
            assert isinstance(child, ObjNode)
            if child.isBasicType() or child.isRefType():
                continue
            code += '%sObject.Init(db)\n' % child.longNamespace()
        code += '}\n'

        code += 'type %s struct {\n' % self.bigLongNamespace()
        code += '// auto bind\n'
        code += 'Id    int64 `json:"id" bson:"_id"`\n'
        code += 'CTime int64 `json:"ctime" bson:"ctime"`\n'
        code += 'MTime int64 `json:"mtime" bson:"mtime"`\n'

        code += '// basic type\n'
        for child in self.children:
            assert isinstance(child, ObjNode)
            if not child.isBasicType(): # basic type
                continue
            jtag = 'json:"%s"' % ('-' if not child.needDump else child.fieldName)
            code += '%s %s `%s bson:"%s"` // %s\n' % (upperFirst(child.fieldName), child.fieldType, jtag, child.fieldName, child.comment)

        code += '// relation\n'
        if self.isMgrType() and not self.parent.isRootObj(): # 如果当前为manager字段，需要引用parent的ID, 且非数组类型
            # print self.parent.lineno, self.parent.isBasicType(), self.fieldName, self.fieldType
            jtag = 'json:"%s"' % ('-' if not self.parent.needDump else '%sId' % self.parent.longNamespace())
            code += 'Ref%sId int64 `%s bson:"%sId"` // %s\n'  % (self.parent.bigLongNamespace(), jtag, self.parent.longNamespace(), self.comment)

        for child in self.children:
            assert isinstance(child, ObjNode)
            if child.isBasicType() or child.isListType(): # skip basic type and list type
                continue
            name = '%s%sId' % ('Mgr' if child.isMgrType() else 'Ref', upperFirst(child.fieldName))
            jtag = 'json:"%s"' % ('-' if not child.needDump else '%sId' % child.fieldName)
            code += '%s int64 `%s bson:"%sId"` // %s\n' % (name, jtag, child.fieldName, child.comment)
        code += '}\n'

        code += 'func New%s(' % self.bigLongNamespace()
        code += self.genObjFuncParam()
        code += ')*%s {\n' % self.bigLongNamespace()
        code += 'return &%s{\n' % self.bigLongNamespace()
        for child in self.children:
            assert isinstance(child, ObjNode)
            if not child.isBasicType(): # basic type
                continue
            code += '%s: %s,\n' % (child.bigShortNamespace(), child.fieldName)
        code += '}\n}\n'
        return code
        
    # import and define
    def objmodel(self):
        code = '// Code generated by m2c. DO NOT EDIT.\npackage db\n'

        code += 'type %sModel struct {\n' % self.bigLongNamespace()
        code += '// auto bind\n'
        code += 'Id    int64 `json:"id" bson:"_id"`\n'
        code += 'CTime int64 `json:"ctime" bson:"ctime"`\n'
        code += 'MTime int64 `json:"mtime" bson:"mtime"`\n'

        code += '// basic type\n'
        for child in self.children:
            assert isinstance(child, ObjNode)
            if not child.isBasicType(): # basic type
                continue
            jtag = 'json:"%s"' % ('-' if not child.needDump else child.fieldName)
            code += '%s %s `%s bson:"%s"` // %s\n' % (upperFirst(child.fieldName), child.fieldType, jtag, child.fieldName, child.comment)

        code += '// relation\n'
        if self.isMgrType() and not self.parent.isRootObj(): # 如果当前为manager字段，需要引用parent的ID, 且非数组类型
            # print self.parent.lineno, self.parent.isBasicType(), self.fieldName, self.fieldType
            jtag = 'json:"%s"' % ('-' if not self.parent.needDump else '%sId' % self.parent.longNamespace())
            code += 'Ref%sId int64 `%s bson:"%sId"` // %s\n'  % (self.parent.bigLongNamespace(), jtag, self.parent.longNamespace(), self.comment)

        for child in self.children:
            assert isinstance(child, ObjNode)
            if child.isBasicType() or child.isListType(): # skip basic type and list type
                continue
            name = '%s%sId' % ('Mgr' if child.isMgrType() else 'Ref', upperFirst(child.fieldName))
            jtag = 'json:"%s"' % ('-' if not child.needDump else '%sId' % child.fieldName)
            code += '%s int64 `%s bson:"%sId"` // %s\n' % (name, jtag, child.fieldName, child.comment)
        code += '}\n'

        code += 'func New%sModel (' % self.bigLongNamespace()
        code += self.genObjFuncParam()
        code += ')*%sModel {\n' % self.bigLongNamespace()
        code += 'return &%sModel{\n' % self.bigLongNamespace()
        for child in self.children:
            assert isinstance(child, ObjNode)
            if not child.isBasicType(): # basic type
                continue
            code += '%s: %s,\n' % (child.bigShortNamespace(), child.fieldName)
        code += '}\n}\n'
        return code

    def objmodelV4(self):
        code = '// Code generated by m2c. DO NOT EDIT.\npackage db\n'

        code += 'type %sModel struct {\n' % self.bigLongNamespace()
        code += '// auto bind\n'
        code += 'Id    interface{} `json:"id" bson:"_id,omitempty"` // int or string \n'
        code += 'CTime int64 `json:"ctime" bson:"ctime"`\n'
        code += 'MTime int64 `json:"mtime" bson:"mtime"`\n'

        code += '// basic type\n'
        for child in self.children:
            assert isinstance(child, ObjNode)
            if not child.isBasicType(): # basic type
                continue
            jtag = 'json:"%s"' % ('-' if not child.needDump else child.fieldName)
            code += '%s %s `%s bson:"%s"` // %s\n' % (upperFirst(child.fieldName), child.fieldType, jtag, child.fieldName, child.comment)

        for child in self.children:
            assert isinstance(child, ObjNode)
            if child.isBasicType() or child.isListType(): # skip basic type and list type
                continue
            if child.isRefType():
                name = '%s%sId' % ('Ref', upperFirst(child.fieldName))
                jtag = 'json:"%s"' % ('-' if not child.needDump else '%sId' % child.fieldName)
                code += '%s int64 `%s bson:"%sId"` // %s\n' % (name, jtag, child.fieldName, child.comment)
            else: # mangerType
                jtag = 'json:"%s"' % ('-' if not child.needDump else '%s' % child.fieldName)
                code += '%s *%sModel `%s bson:"%s"` // %s\n' % (upperFirst(child.fieldName), upperFirst(child.bigLongNamespace()), jtag, child.fieldName, child.comment)
        code += '}\n'

        code += 'func New%sModel (' % self.bigLongNamespace()
        code += self.genObjFuncParam()
        code += ')*%sModel {\n' % self.bigLongNamespace()
        code += 'return &%sModel{\n' % self.bigLongNamespace()
        for child in self.children:
            assert isinstance(child, ObjNode)
            if not child.isBasicType(): # basic type
                continue
            code += '%s: %s,\n' % (child.bigShortNamespace(), child.fieldName)
        code += '}\n}\n'

        code += self.objmodelV4Mgr()
        return code

    def objmodelV4Mgr(self):
        code = ''
        for child in self.children:
            if not child.isMgrType():
                continue

            code += 'type %sModel struct {\n' % child.bigLongNamespace()
            code += '// basic type\n'
            for subchild in child.children:
                assert isinstance(subchild, ObjNode)
                if not subchild.isBasicType(): # basic type
                    continue
                jtag = 'json:"%s"' % ('-' if not subchild.needDump else subchild.fieldName)
                code += '%s %s `%s bson:"%s"` // %s\n' % (upperFirst(subchild.fieldName), subchild.fieldType, jtag, subchild.fieldName, subchild.comment)

            for subchild in child.children:
                assert isinstance(subchild, ObjNode)
                if subchild.isBasicType() or subchild.isListType(): # skip basic type and list type
                    continue

                jtag = 'json:"%s"' % ('-' if not subchild.needDump else subchild.fieldName)
                code += '%s *%sModel `%s bson:"%s"` // %s\n' % (upperFirst(subchild.fieldName), upperFirst(subchild.bigLongNamespace()), jtag, subchild.fieldName, subchild.comment)
            code += '}\n'

            code += 'func New%sModel (' % child.bigLongNamespace()
            code += child.genObjFuncParam()
            code += ')*%sModel {\n' % child.bigLongNamespace()
            code += 'return &%sModel{\n' % child.bigLongNamespace()
            for subchild in child.children:
                assert isinstance(subchild, ObjNode)
                if not subchild.isBasicType(): # basic type
                    continue
                code += '%s: %s,\n' % (subchild.bigShortNamespace(), subchild.fieldName)
            code += '}\n}\n'

            code += child.objmodelV4Mgr()
        return code

    # fixed code by tpl
    def objCode1(self):
        name = self.longNamespace()
        bigname = name[0].upper() + name[1:]
        code = OBJ_FIXED_CODE_TPL.replace('$BIGOBJNAME$', bigname)
        code = code.replace('$OBJNAME$', name)
        return code

    # helper func for relation
    def objCode2(self):
        code = ''
        name = self.longNamespace()
        bigname = upperFirst(name)
        for child in self.children:
            assert isinstance(child, ObjNode)
            code + '\n'
            fieldName = child.fieldName if child.isBasicType() else '%sId' % child.fieldName
            fieldType = child.fieldType if child.isBasicType() else 'int64'
            values = {'objName': name, 'ObjName': bigname, 'fieldName': fieldName, 'FieldName': upperFirst(fieldName), 'filedType': fieldType}
            code += OBJ_FILTER_HELPER_CODE_TPL.format(**values)
            code + '\n'

            # string type add like
            if fieldType == 'string':
                code += OBJ_FILTER_STRING_LIKE_CODE_TPL.format(**values)
                code += '\n'

        for child in self.children:
            assert isinstance(child, ObjNode)
            if not child.isMgrType(): 
                continue
            code += '\n'
            # print child.fieldName, child.fieldType, child.lineno
            values = {'objName': name, 'ObjName': bigname, 'mgrShortName': child.shortNamespace(), 'MgrShortName': child.bigShortNamespace(), 'mgrLongName': child.longNamespace(), 'MgrLongName': child.bigLongNamespace()}
            # print child.fieldName, child.fieldType
            if child.isListType():
                code += OBJ_MGR_ARRAY_HELPER_CODE_TPL.format(**values)
            else:
                code += OBJ_MGR_HELPER_CODE_TPL.format(**values)
            code += '\n'

        for child in self.children:
            assert isinstance(child, ObjNode)
            if not child.isRefType(): 
                continue
            code += '\n'
            values = {'objName': name, 'ObjName': bigname, 'refName': child.shortNamespace(), 'RefName': child.bigShortNamespace()}
            code += OBJ_REF_HELPER_CODE_TPL.format(**values)
            code += '\n'

        if self.isMgrType() and not self.parent.isRootObj():
            code += '\n'
            values = {'objName': name, 'ObjName': bigname, 'parentName': self.parent.longNamespace(), 'ParentName': self.parent.bigLongNamespace()}
            code += OBJ_MGR_REF_HELPER_CODE_TPL.format(**values)
            code += '\n'
        return code

    def objCodeFull(self):
        if self.isBasicType() or self.isRefType():
            return None
        return self.objCode0() + self.objCode1() + self.objCode2()

    def objCodeFullV4(self):
        return self.objmodelV4()

    def objCodePath(self):
        if self.isBasicType() or self.isRefType():
            return None
        p = '%s/%s.go' % (self.fieldName, self.fieldName)
        parent = self.parent
        while parent:
            if parent.lineno == 1:
                break
            assert isinstance(parent, ObjNode)
            p = os.path.join(parent.fieldName, p)
            parent = parent.parent
        return 'gopher/ak/orm/%s' % p

    def objCodePathV4(self):
        if self.isBasicType() or self.isRefType():
            return None
        p = '%s.go' % (self.fieldName)
        parent = self.parent
        while parent:
            if parent.lineno == 1:
                break
            assert isinstance(parent, ObjNode)
            p = parent.fieldName + '_' + p
            parent = parent.parent
        return '%s/model/db/%s' % (MOD, p)
    
    def objImportPath(self):
        p = self.fieldName
        if not self.isRefType():
            parent = self.parent
            while parent:
                if parent.lineno == 1:
                    break
                assert isinstance(parent, ObjNode)
                p = os.path.join(parent.fieldName, p)
                parent = parent.parent
        return 'gopher/ak/orm/%s' % p

    def getObjs(self):
        if self.isBasicType() or self.isRefType():
            return []

        # real object
        objs = []
        objFields = [('id', False, 'int64', '对象ID'), ('ctime', False, 'int64', '对象创建时间'), ('mtime', False, 'int64', '对象修改时间')]

        # basic type
        for child in self.children:
            assert isinstance(child, ObjNode)
            if not child.isBasicType(): # basic type
                continue
            
            if not child.needDump:
                continue
            objFields.append((child.fieldName, child.isListType(), child.fieldType, child.comment))

        # relation fields
        if self.isMgrType() and not self.parent.isRootObj(): # 如果当前为manager字段，需要引用parent的ID
            if self.parent.needDump:
                objFields.append(('%sId' % self.parent.longNamespace(), False, 'int64', self.parent.comment))

        for child in self.children:
            assert isinstance(child, ObjNode)
            if child.isBasicType(): # skip basic type
                continue
            if not child.needDump:
                continue
            name = '%s%sId' % ('Mgr' if child.isMgrType() else 'Ref', upperFirst(child.fieldName))
            objFields.append(('%sId' % child.fieldName, False, 'int64', child.comment))

        obj = (self.bigLongNamespace(), self.comment, objFields)
        objs.append(obj)

        # nest parser child mgr type
        for child in self.children:
            assert isinstance(child, ObjNode)
            if not child.isMgrType():
                continue
            objs.extend(child.getObjs())

        return objs
    
    def genObjCode(self):
        codePath = self.objCodePath()
        if codePath:
            print(codePath)
            code = self.objCodeFull()
            writeCode(codePath, code)

        for child in self.children:
            child.genObjCode()

    def genObjCodeV4(self):
        codePath = self.objCodePathV4()
        if codePath:
            print(codePath)
            code = self.objCodeFullV4()
            writeCode(codePath, code)

        # for child in self.children:
        #     child.genObjCodeV4()

    def checkObjCode(self, topObjects):
        for child in self.children:
            if child.isRefType():
                assert_error(child.fieldType in topObjects, '引用对象%s未定义' % child.fieldType, child.lineno, child.line)
            child.checkObjCode(topObjects)

    @classmethod
    def fromLine(cls, lineno, line, parent):
        assert isinstance(parent, ObjNode)
        if '#' in line:
            itemsline, comment = line.split('#', 1)
            items = itemsline.split()
        else:
            comment = ''
            items = line.split()
        assert_error(len(items) in (2, 3), '无效的object定义', lineno, line)
        needDump = True
        if len(items) == 3:
            needDump = False
        fieldName = items[0]
        assert_error(is_valid_var(fieldName), '关键字必须是有效变量', lineno, line)
        pobjs = items[1] # [Prefix]objtyp[Subfix]
        refType = OBJ_REF_TYPE_MANAGE
        if pobjs[0] in ('@', '&'):
            refType = OBJ_REF_TYPE_REF
        if pobjs[0] == '^':
            needDump = False
        containerType = OBJ_CONTAINER_TYPE_SINGLE
        _isList = False
        if pobjs.endswith('[]'):
            _isList = True
            containerType = OBJ_CONTAINER_TYPE_LIST
        
        fieldType = pobjs.strip('[]').strip('@').strip('&').strip('^')
        if fieldType in TYPEMAPPING:
            containerType = OBJ_CONTAINER_TYPE_BASIC
            refType = OBJ_CONTAINER_TYPE_BASIC
            fieldType = TYPEMAPPING[fieldType] # rel gotype
            if _isList:
                fieldType = '[]' + fieldType
        
        return cls(lineno, line, fieldName, fieldType, containerType, refType, needDump, comment, parent)

class ObjModel(object):
    def __init__(self, root):
        self.root = root

    @classmethod
    def fromLines(cls, lines):
        startline = lineno = 1
        root = lines[0].strip()
        items = root.split(' ')
        assert_error(len(items) == 2, '无效的objmodel', lineno, root)
        assert_error(items[0] == 'root' and items[1] == 'object', '无效的objmodel', lineno, root)

        rootNode = ObjNode(lineno, root, None, None, None, None, False, None, None)
        deepParents = [rootNode]
        for line in lines[startline:]:
            lineno += 1

            if not line.strip():
                continue
            
            width = len(line) - len(line.lstrip())
            assert_error(width % TAB_WIDTH == 0, '宽度不是%s的倍数' % TAB_WIDTH, lineno, line)
            deep = int(width / TAB_WIDTH) # py2 vs py3
            assert_error(deep <= OBJ_MAX_DEEP_NUM, '深度不能超过%s' % OBJ_MAX_DEEP_NUM, lineno, line)

            line = line.strip()

            if deep == 0: # new top object
                # print 'add top object %s, %s' % (lineno, line)
                parentNode = deepParents[deep]
                node = ObjNode.fromLine(lineno, line, parentNode)
                if not node:
                    continue
                parentNode.addChildren(node)
                deepParents = [rootNode]
                deepParents.append(node)
            elif deep <= len(deepParents):
                parentNode = deepParents[deep]
                node = ObjNode.fromLine(lineno, line, parentNode)
                if not node:
                    continue
                # print 'add child %s to %s' % (node.fieldName, parentNode.fieldName)
                parentNode.addChildren(node)
                deepParents = deepParents[:deep+1]
                deepParents.append(node)
            else:
                assert_error(False, '缩进有问题 %s, 当前最大 %s' % (deep, len(deepParents)), lineno, line)

        return cls(rootNode)
    
    def checkObjCode(self):
        topObjects = []
        for child in self.root.children:
            topObjects.append(child.fieldType)
        
        for child in self.root.children:
            child.checkObjCode(topObjects)
        print('check done.')

    def genObjInitCode(self):
        code = '// Code generated by m2c. DO NOT EDIT.\npackage orm\n'
        code += 'import (\n'
        code += 'id "%s/ak/orm/id"\n' % MOD
        for child in self.root.children:
            assert isinstance(child, ObjNode)
            code += '%sObject "%s/ak/orm/%s"\n' % (child.fieldName, MOD, child.fieldName)
        code += ')\n'
        code += 'func Init(url string) {\n'
        code += 'mgoDB := getMGO(url)\n'
        code += 'id.Init(mgoDB)\n'
        for child in self.root.children:
            assert isinstance(child, ObjNode)
            code += '%sObject.Init(mgoDB)\n' % child.fieldName
        code += '}\n'
        code += '// DropDB Dangers!!!\n'
        code += 'func DropDB(url string) {\n'
        code += 'mgoDB := getMGO(url)\n'    
        code += 'mgoDB.DropDatabase()\n'
        code += '}\n'
        return code

    def genObjCode(self):
        # api model
        initPath = 'gopher/ak/orm/init.go'
        writeCode(initPath, self.genObjInitCode())

        # api group
        for child in self.root.children:
            assert isinstance(child, ObjNode)
            child.genObjCode()

        # api doc
        os.system('gofmt -w gopher/ak/orm')

    def genObjCodeV4(self):
        # db model
        for child in self.root.children:
            assert isinstance(child, ObjNode)
            child.genObjCodeV4()

        # api doc
        os.system('gofmt -w %s/model' % MOD)
