import * as Knex from 'knex';
import {Container} from 'typescript-ioc';
import 'zone.js';
import { v4 } from 'uuid';
import {DataState, FieldInfo} from 'model-util-base'

interface ModelUtil<CU, MI>{
    tableFields: string[];
    modelFields: string[];    
    createConditionUtil(): CU;    
    select(...args: any[]): SelectStatement<CU, MI>;
    selectOne(...args: any[]): SelectStatement<CU, MI>;
    joinTables: JoinTable[];
    queryRaw(expression: string, ...args): Promise<any>;
    querySingleRaw(expression: string, ...args): Promise<any>;
    querySingleValueRaw(expression: string, ...args): Promise<any>;
    updateRaw(expression: string, ...args): Promise<any>;
}

abstract class ModelUtilImpl<CU, MI> implements ModelUtil<CU, MI>{    
    abstract selectSQL: string;    
    abstract tableFields: string[];
    abstract modelFields: string[];
    abstract createConditionUtil(): CU;
    abstract joinTables: JoinTable[];

    protected knex: Knex;                   

    constructor(){        
        this.knex = Container.get(Knex as any);           
    }        

    select(...fields: any[]): SelectStatement<CU, MI>{
        if(fields != null && Array.isArray(fields[0])){
            fields = fields[0];
        }
        
        return new SelectStatement(this.selectSQL, this, false, this.knex, fields);
    }

    selectOne(...fields: any[]): SelectStatement<CU, MI>{
        if(fields != null && Array.isArray(fields[0])){
            fields = fields[0];
        }
        
        return new SelectStatement(this.selectSQL, this, true, this.knex, fields);
    }

    selectByParameters(parameters: SelectParameters): SelectStatement<CU, MI>{        
        let result = new SelectStatement(this.selectSQL, this, false, this.knex, parameters.fields);
        result.expression = parameters.where;
        result.whereType = parameters.whereType;
        result.limitN = parameters.limit.max;
        result.limitOffset = parameters.limit.offset;
        result.args = parameters.args;
        result.isCount = parameters.isCount;
        return result;
    }

    async queryRaw(expression: string, ...args): Promise<any>{
        return new Promise((resolver, reject)=>{
            let tx:Knex.Transaction = Zone.current.get('tx');            
            let raw = this.knex.raw(expression, args);
            if(tx != null && tx != undefined){
                raw.transacting(tx);
            }

            raw.then((value)=>{
                resolver(value[0])
            }, reject)
        });
    }

    async querySingleRaw(expression: string, ...args): Promise<any>{
        return new Promise((resolver, reject)=>{
            let tx:Knex.Transaction = Zone.current.get('tx');            
            let raw = this.knex.raw(expression, args);
            if(tx != null && tx != undefined){
                raw.transacting(tx);
            }

            raw.then((value)=>{
                if(value[0]&& value[0][0]){
                    resolver(value[0][0]);
                    return;
                }
                
                resolver(null);
            }, reject)
        });
    }

    async querySingleValueRaw(expression: string, ...args): Promise<any>{
        return new Promise((resolver, reject)=>{
            let tx:Knex.Transaction = Zone.current.get('tx');            
            let raw = this.knex.raw(expression, args);
            if(tx != null && tx != undefined){
                raw.transacting(tx);
            }

            raw.then((value)=>{
                if(value[0]&& value[0][0]){
                    for(let key in value[0][0]){
                        resolver(value[0][0][key])
                        return;
                    }
                }
                
                resolver(null);
            }, reject)
        });
    }

    async updateRaw(expression: string, ...args): Promise<any>{
        return new Promise((resolver, reject)=>{
            let tx:Knex.Transaction = Zone.current.get('tx');            
            let raw = this.knex.raw(expression, args);
            if(tx != null && tx != undefined){
                raw.transacting(tx);
            }

            raw.then((value)=>{
                resolver(value[0]['affectedRows'])
            }, reject)
        });
    }
}

interface ConditionSource{
    expression: string;    
    args?: any[];
    isCount: boolean;
    limitN?: number;
    limitOffset?: number;        
}

interface Statement<P> extends ConditionSource{
    type: string;
    asParameters(): P;
}

enum WhereType{
    None = 0,
    Where = 1,
    WhereAnd = 2,
    WrapWhere = 3    
}

interface SelectAndCountResult{
    data: any;
    count: number;
}

class SelectStatement<CU, MI> implements Statement<SelectParameters>, PromiseLike<any>{        
    type: 'select';
    private knex?: Knex;      

    expression = '';
    originalSQL = null;
    args?: any[] = [];
    isCount = false;
    isWithCount = false;
    isSelectById = false;
    isSelectByIds = false;
    hasOrderBy = false;
    orderByExpression?: string;
    modelUtil: ModelUtil<CU, MI>;
    limitN?: number;
    limitOffset?: number;
    fields?: FieldUtil<CU>[] = null;
    whereType: WhereType = WhereType.None;
    onlyOne: boolean = false;
    
    private joinTables: JoinTable[] = null;
    private subTables: SubTable[] = null;

    private manualJoinTables = {};
    private cancelJoinTables = {};
    private subTablesToFetch = {};

    private joinJobs:JoinTable[] = [];
    private finishedJoins = [];

    private subTableJobs: SubTable[] = [];
    private finishedSubTables = [];        

    constructor(sql: string, modelUtil: ModelUtil<CU, MI>, onlyOne: boolean, knex?: Knex, fields?: FieldUtil<CU>[]){
        this.knex = knex;         
        this.originalSQL = sql;        
        this.modelUtil = modelUtil;
        this.onlyOne = onlyOne;

        if(this.modelUtil instanceof TableUtilImpl){
            this.joinTables = this.modelUtil.joinTables;
            this.subTables = this.modelUtil.subTables;
        }

        this.fields = fields;        
    }  

    then(
        onfulfilled?: (value: any) => any, 
        onrejected?: (reason: any) => any ): any{ 
        return this.doQuery().then(onfulfilled, onrejected);
    }    

    async doQuery(): Promise<any>{
        return new Promise(async (resolver, reject)=>{
            try{
                let sql = this.createSQL();                                     

                if(this.hasOrderBy){
                    sql = sql + ' order by ' + this.orderByExpression;
                }            

                if(this.isCount){                
                    resolver(await this.executeCount(sql));
                }               
                else{
                    let count = 0;
                    if(this.isWithCount){
                        count = await this.executeCount(sql);
                    }

                    if(this.limitN != null){                                            
                        if(this.limitOffset != null && this.limitOffset != undefined){
                            sql += ' limit ' + this.limitOffset + ' , ' + this.limitN; 
                        }
                        else{
                            sql += ' limit ' + this.limitN; 
                        }
                    }
                    let data = await this.executeQuery(sql);   
                    
                    if(this.isWithCount){
                        let result = {
                            count: count,
                            data: data
                        } as SelectAndCountResult;
                        resolver(result);
                    }
                    else{
                        resolver(data);
                    }
                }     
            }
            catch(error){
                reject(error);
            }            
        })        
    }    

    private async executeCount(sql: string):Promise<any>{
        sql = 'select count(1) CNT from (' + sql + ") tmp";
        return new Promise((resolver, reject)=>{
            let tx:Knex.Transaction = Zone.current.get('tx');
            let raw = this.knex.raw(sql, this.args);
            if(tx != null && tx != undefined){
                raw.transacting(tx);
            }

            raw.then((value: any)=>{
                let result = null;
                let joinFlag = false;
                let subTableFlag = false;
                
                resolver(value[0][0]['CNT']);
            }, reject);
        })
    }

    private async executeQuery(sql: string):Promise<any>{
        let onlyOne = this.onlyOne;
        return new Promise((resolver, reject)=>{
            let tx:Knex.Transaction = Zone.current.get('tx');
            let raw = this.knex.raw(sql, this.args);
            if(tx != null && tx != undefined){
                raw.transacting(tx);
            }
            
            raw.then((value: any)=>{
                let result = null;
                let joinFlag = false;
                let subTableFlag = false;
                if(onlyOne){                    
                    if(Array.isArray(value[0]) && value[0].length >0){
                        result = [this.processSingleResult(value[0][0])];
                    }                    
                }
                else{
                    if(value[0] instanceof Array){
                        result = this.processArrayResult(value[0]);                                     
                    }
                    else{
                        result = [this.processSingleResult(value[0])];
                    }
                }

                if(!result){
                    resolver(null);
                    return;
                }

                if(this.joinTables != null && this.joinTables.length > 0){
                    joinFlag = true;
                    this.doJoinTables(result).then(()=>{
                        joinFlag = false;
                        if(!subTableFlag){
                            this.doResolve(resolver, result)
                        }
                    }, reject);
                }   
                if(this.subTables != null && this.subTables.length > 0){
                    subTableFlag = true;
                    this.fetchSubTables(result).then(()=>{
                        subTableFlag = false;
                        if(!joinFlag){
                            this.doResolve(resolver, result)
                        }
                    }, reject);
                }           

                if(!joinFlag && !subTableFlag){
                    this.doResolve(resolver, result)
                }
            }, reject);
        })
    }

    private doResolve(resolver: any, result: any){
        if(this.onlyOne){
            resolver(result[0]);
        }
        else{
            resolver(result);
        }
    }

    private createSQL():string{    
        let sql = this.originalSQL;
        let fieldsString = null;    
        if(this.fields.length > 0){
            for(let i=0; i< this.fields.length; i++){
                if(i == 0){
                    fieldsString = this.fields[i].tableField;
                }
                else{
                    fieldsString += ',' + this.fields[i].tableField;
                }   
            }
        }

        if(this.modelUtil instanceof TableUtilImpl){
            let tableUtil: TableUtilImpl<CU,MI> = this.modelUtil;            
            if(fieldsString == null){                         
                for(let i=0; i< tableUtil.tableFields.length; i++){
                    if(i == 0){
                        fieldsString = tableUtil.tableFields[i];
                    }
                    else{
                        fieldsString += ',' + tableUtil.tableFields[i];
                    }   
                }                    
            }

            sql = 'select ' + fieldsString + ' from ' + tableUtil.tableName;    
        }

        switch(this.whereType){
            case WhereType.Where:{
                sql = sql + ' where ' + this.expression;          
                break;      
            }
            case WhereType.WhereAnd:{
                sql = sql + ' and ' + this.expression;                
                break;
            }
            case WhereType.WrapWhere:{
                if(fieldsString != null){
                    sql = ' select ' + fieldsString + ' from (' + sql + ') tmp where ' + this.expression;
                }
                else{
                    sql = ' select * from (' + sql + ') tmp where ' + this.expression;               
                }
                break; 
            }
        }        

        return sql;
    }

    private processSingleResult(record: any): MI{
        let value = {};
        
        if(this.fields != null && this.fields.length> 0){
            for(let i=0; i < this.fields.length; i ++){
                value[this.fields[i].modelField] = record[this.fields[i].tableField];
            }
        }
        else{
            for(let i=0; i < this.modelUtil.tableFields.length; i ++){
                value[this.modelUtil.modelFields[i]] = record[this.modelUtil.tableFields[i]];
            }
        }

        return value as MI;        
    }

    private processArrayResult(records: any[]): MI[]{
        if(records.length == 0)
            return [];
        
        let recordKeys = [];        
        for(let key in records[0]){            
            recordKeys[recordKeys.length] = key;             
        }

        let result = [];
        for(let i = 0; i< records.length; i ++){
            let value = {};

            if(this.fields != null && this.fields.length> 0){
                for(let j=0; j < this.fields.length; j++){
                    value[this.fields[j].modelField] = records[i][this.fields[j].tableField];
                }
            }
            else{
                for(let j=0; j < this.modelUtil.modelFields.length; j++){
                    value[this.modelUtil.modelFields[j]] = records[i][recordKeys[j]];
                }
            }
            
            result.push(value as MI);
        }
        return result as MI[];        
    }

    private doJoinTables(data: any[]): Promise<any>{
        for(let i=0;i<this.joinTables.length;i++){
            let joinTable = this.joinTables[i];  
            let muName = joinTable.getTableUtil().constructor.name;
            if(joinTable.joinByDefault){
                if(!this.cancelJoinTables[muName]){ 
                    this.joinJobs[this.joinJobs.length] = joinTable;               
                }                                
            }
            else{ 
                if(this.manualJoinTables[muName]){
                    this.joinJobs[this.joinJobs.length] = joinTable;
                }
            }
        }

        return new Promise((resolver, reject) =>{
            if(this.joinJobs.length <= 0){
                resolver();
                return;
            }

            for(let i=0;i<this.joinJobs.length;i++){
                let joinTable = this.joinJobs[i];            
                
                let tableFields: FieldUtil<CU>[] = [];
                let tableFieldsMap = {};
                this.generateJoinFields(joinTable, tableFields, tableFieldsMap);
                            
                let args = [];
                let argsMap = {};            
                let fkFilter = this.generateJoinExpression(args, argsMap, data, joinTable.foreignKeyFields, joinTable.joinIdFields);            
                if(fkFilter == null){
                    this.finishedJoins[this.finishedJoins.length] = joinTable;
                    if(this.finishedJoins.length == this.joinJobs.length){
                        resolver();
                        return;
                    }
                    else{
                        continue;
                    }
                }
                    
                let selectStatement = joinTable.getTableUtil().select(tableFields);
                selectStatement.manualJoinTables = this.manualJoinTables;
                selectStatement.cancelJoinTables = this.cancelJoinTables;
                selectStatement.cancelJoinTables[this.modelUtil.constructor.name] = 1;
                selectStatement.where(fkFilter, args).then((joinResult)=>{
                        let joinResultMap = [];
                        for(let j=0;j<joinResult.length; j++){
                            let record = joinResult[j];
                            let keyString = this.generateKeyString(record, joinTable.joinIdFields);                
                            joinResultMap[keyString] = record;
                        }
            
                        for(let j=0;j<data.length; j++){                
                            let keyString = this.generateKeyString(data[j], joinTable.foreignKeyFields);                   
                            if(keyString == null)
                                continue;                
                            
                            let record = joinResultMap[keyString];
                            if(record != null && record != undefined){
                                for(let k=0;k<joinTable.fields.length; k++){
                                    data[j][joinTable.fields[k].alias] = record[joinTable.fields[k].fieldUtil.modelField];
                                }
                            }                
                        }
                        
                        this.finishedJoins[this.finishedJoins.length] = joinTable;
                        if(this.finishedJoins.length == this.joinJobs.length){
                            resolver();
                        }
                    },reject);
            }
        });        
    }

    private generateJoinFields(joinTable: JoinTable,tableFields: FieldUtil<CU>[], tableFieldsMap: {}){        
        for(let j=0; j<joinTable.joinIdFields.length; j++){
            tableFields[tableFields.length] = joinTable.joinIdFields[j];
            tableFieldsMap[joinTable.joinIdFields[j].tableField] = 1;
        }                        
        for(let j=0;j<joinTable.fields.length; j++){
            if(joinTable.fields[j].fieldUtil instanceof FieldUtil){
                let tableField = joinTable.fields[j].fieldUtil as FieldUtil<any>;
                if(tableFieldsMap[tableField.tableField] == null || tableFieldsMap[tableField.tableField] == undefined){
                    tableFields[tableFields.length] = tableField;  
                    tableFieldsMap[tableField.tableField] = 1; 
                }
            }               
        }

        let nextJoinTabls = joinTable.getTableUtil().joinTables;
        if(nextJoinTabls != null){
            for(let j=0;j<nextJoinTabls.length;j++){
                for(let k=0; k<joinTable.joinIdFields.length; k++){
                    let fieldUtil = nextJoinTabls[j].foreignKeyFields[k];
                    if(tableFieldsMap[fieldUtil.tableField] == null || tableFieldsMap[fieldUtil.tableField] == undefined){
                        tableFields[tableFields.length] = fieldUtil;  
                        tableFieldsMap[fieldUtil.tableField] = 1; 
                    }                        
                }                                          
            }
        }
    }

    private generateJoinExpression(args:any[], argsMap: {}, data: any[], 
        leftTableKeyFields: FieldUtil<any>[], rightTableKeyFields: FieldUtil<any>[]): string{
        let fkFilter = null;
                  
        for(let j=0;j<data.length; j++){                      
            let keyString = this.generateKeyString(data[j], leftTableKeyFields);                
            if(keyString == null)
                continue;
            if(argsMap[keyString] != null && argsMap[keyString] != undefined)
                continue;  
                
            argsMap[keyString] = 1;     
            if(fkFilter != null){
                fkFilter += ' or (';
            }
            else{
                fkFilter = '(';
            }           

            for(let k=0;k<leftTableKeyFields.length;k++){
                let value = data[j][leftTableKeyFields[k].modelField];
                args[args.length] = value;

                if(k > 0)
                    fkFilter += ') and (';
                fkFilter += rightTableKeyFields[k].tableField + ' = ?)';                    
            }
        }
        return fkFilter;
    }

    private generateKeyString(data: any, fields: FieldUtil<any>[]): string{
        let hasKey = true;      
        let keyString = null;
        for(let i=0;i<fields.length;i++){
            let value = data[fields[i].modelField];            
            if(value == null || value == undefined){
                hasKey = false;
                break;
            }
            if(keyString == null){
                keyString = value;
            }
            else{
                keyString += ',' + value;
            }
        }
        if(!hasKey)
            return null;
        else
            return keyString;        
    }

    private fetchSubTables(data: any[]): Promise<any>{
        for(let i=0;i<this.subTables.length;i++){
            let subTable = this.subTables[i];            
            let muName = subTable.getTableUtil().constructor.name;
            if(this.subTablesToFetch[muName]){
                this.subTableJobs[this.subTableJobs.length] = subTable;
            }
        }

        return new Promise((resolver, reject) =>{
            if(this.subTableJobs.length <= 0){
                resolver();
                return;
            }

            for(let i=0;i<this.subTableJobs.length;i++){
                let subTable = this.subTableJobs[i];            
                                                            
                let args = [];
                let argsMap = {};            
                let fkFilter = this.generateJoinExpression(args, argsMap, data, subTable.parentIdFields, subTable.foreignKeyFields);            
                if(fkFilter == null){
                    this.finishedSubTables[this.finishedSubTables.length] = subTable;
                    if(this.finishedJoins.length == this.joinJobs.length){
                        resolver();
                        return;
                    }
                    else{
                        continue;
                    }
                }
                    
                let selectStatement = subTable.getTableUtil().select();
                selectStatement.manualJoinTables = this.manualJoinTables;
                selectStatement.cancelJoinTables = this.cancelJoinTables;
                selectStatement.cancelJoinTables[this.modelUtil.constructor.name] = 1;
                selectStatement.subTablesToFetch = this.subTablesToFetch;
                selectStatement.where(fkFilter, args).then((queryResult)=>{
                        let subTableRecordMap = {};
                        for(let j=0;j<queryResult.length; j++){
                            let record = queryResult[j];
                            let keyString = this.generateKeyString(record, subTable.foreignKeyFields);   
                            if(!subTableRecordMap[keyString]){
                                subTableRecordMap[keyString] = [];
                            }    
                            let subTableRecords = subTableRecordMap[keyString];      
                            subTableRecords[subTableRecords.length] = record;
                        }
            
                        for(let j=0;j<data.length; j++){                
                            let keyString = this.generateKeyString(data[j], subTable.parentIdFields);                   
                            if(keyString == null)
                                continue;                
                            
                            if(subTableRecordMap[keyString]){
                                data[j][subTable.name] = subTableRecordMap[keyString];
                            }
                        }
                        
                        this.finishedSubTables[this.finishedSubTables.length] = subTable;
                        if(this.finishedSubTables.length == this.subTableJobs.length){
                            resolver();
                        }
                    },reject);                    
            }
        }); 
    }            

    initArgs(...initArgs: any[]){
        this.args = initArgs;
    }

    join(...joinTables: Function[]): SelectStatement<CU,MI>{
        if(joinTables != null && joinTables != undefined){
            for(let i=0; i< joinTables.length; i++){                
                this.manualJoinTables[joinTables[i].name] = 1;
            }
        }

        return this;
    }

    notJoin(...joinTables: Function[]): SelectStatement<CU,MI>{
        if(joinTables != null && joinTables != undefined){
            for(let i=0; i< joinTables.length; i++){
                this.cancelJoinTables[joinTables[i].name] = 1;
            }
        }

        return this;
    }

    withSubtables(...subTables: Function[]): SelectStatement<CU,MI>{
        if(subTables != null && subTables != undefined){
            for(let i=0; i< subTables.length; i++){
                if(subTables[i]){
                    this.subTablesToFetch[subTables[i].name] = 1;
                }
            }
        }

        return this;
    }

    where(builder: (cu: CU)=> void ): ConditionParenthesis<CU, SelectParameters>;
    where(builder: ConditionBuilder<any>): ConditionParenthesis<CU, SelectParameters>;
    where(builder: string, ...args: any[]): ConditionParenthesis<CU, SelectParameters>;
    where(builder, ...args: any[]): ConditionParenthesis<CU, SelectParameters>{       
        this.whereType = WhereType.Where;
        if(args != null && Array.isArray(args[0])){
            args = args[0];
        }

        if(builder instanceof ConditionBuilderImpl){
            return new ConditionParenthesis<CU, SelectParameters>(this, '(', ') ', builder);
        }
        else if(typeof builder === "string" ){
            let cu = this.modelUtil.createConditionUtil();            
            return new ConditionParenthesis<CU, SelectParameters>(this, '', '', 
                (cu as unknown as ConditionUtilImpl<CU>).conditionBuilder.append(builder, args));            
        }
        else{
            let cu = this.modelUtil.createConditionUtil();
            builder(cu);
            return new ConditionParenthesis<CU, SelectParameters>(this, '(', ') ',
                (cu as unknown as ConditionUtilImpl<any>).conditionBuilder as ConditionBuilderImpl<CU>);
        }
    }

    whereAnd(builder: (cu: CU)=> void ): ConditionParenthesis<CU, SelectParameters>;
    whereAnd(builder: ConditionBuilder<any>): ConditionParenthesis<CU, SelectParameters>;               
    whereAnd(builder: string, ...args: any[]): ConditionParenthesis<CU, SelectParameters>;
    whereAnd(builder, ...args: any[]): ConditionParenthesis<CU, SelectParameters>{  
        this.whereType = WhereType.WhereAnd;
        if(args != null && Array.isArray(args[0])){
            args = args[0];
        }

        if(builder instanceof ConditionBuilderImpl){
            return new ConditionParenthesis<CU, SelectParameters>(this, '(', ') ', builder);
        }
        else if(typeof builder === "string" ){
            let cu = this.modelUtil.createConditionUtil();            
            return new ConditionParenthesis<CU, SelectParameters>(this, '', '', 
                (cu as unknown as ConditionUtilImpl<CU>).conditionBuilder.append(builder, args));            
        }
        else{
            let cu = this.modelUtil.createConditionUtil();
            builder(cu);
            return new ConditionParenthesis<CU, SelectParameters>(this, '(', ') ',
                (cu as unknown as ConditionUtilImpl<any>).conditionBuilder as ConditionBuilderImpl<CU>);
        }
    }

    wrapWhere(builder: (cu: CU)=> void ): ConditionParenthesis<CU, SelectParameters>;
    wrapWhere(builder: ConditionBuilder<any>): ConditionParenthesis<CU, SelectParameters>;               
    wrapWhere(builder: string, ...args: any[]): ConditionParenthesis<CU, SelectParameters>;
    wrapWhere(builder, ...args: any[]): ConditionParenthesis<CU, SelectParameters>{    
        this.whereType = WhereType.WrapWhere;
        if(args != null && Array.isArray(args[0])){
            args = args[0];
        }

        if(builder instanceof ConditionBuilderImpl){
            return new ConditionParenthesis<CU, SelectParameters>(this, '(', ') ', builder);
        }
        else if(typeof builder === "string" ){
            let cu = this.modelUtil.createConditionUtil();            
            return new ConditionParenthesis<CU, SelectParameters>(this, '', '', 
                (cu as unknown as ConditionUtilImpl<CU>).conditionBuilder.append(builder, args));            
        }
        else{
            let cu = this.modelUtil.createConditionUtil();
            builder(cu);
            return new ConditionParenthesis<CU, SelectParameters>(this, '(', ') ',
                (cu as unknown as ConditionUtilImpl<any>).conditionBuilder as ConditionBuilderImpl<CU>);
        }
    }

    count(): SelectStatement<CU, MI>{
        this.isCount = true;
        return this;
    }    

    limit(... args): SelectStatement<CU, MI>{
        if(args.length == 1){
            this.limitN = args[0];
        }
        else if(args.length == 2){            
            this.limitOffset = args[0];
            this.limitN = args[1];
        }
                
        return this;
    }

    limitAndCount(... args): SelectStatement<CU, MI>{        
        if(args.length == 1){
            this.limitN = args[0];
        }
        else if(args.length == 2){            
            this.limitOffset = args[0];
            this.limitN = args[1];
        }

        this.isWithCount = true;
        return this;
    }

    orderBy(expression: string): SelectStatement<CU, MI>{
        this.hasOrderBy = true;
        this.orderByExpression = expression;
        return this;
    }

    asParameters(): SelectParameters{
        let result = {} as SelectParameters;
        result.args = this.args;
        result.isCount = this.isCount;
        result.fields = this.fields;
        result.limit.max = this.limitN;
        result.limit.offset = this.limitOffset;
        result.where = this.expression;
        return result;
    }
}

class ConditionParenthesis<CU,P>{
    source: ConditionSource;              
    args:any[] = [];
    conditionBuilder: ConditionBuilder<CU>;        
    conditionUtil: ConditionUtilImpl<CU>;

    constructor(source: ConditionSource, start: string, end: string, 
        conditionBuilder: ConditionBuilder<CU>){        
        this.source = source;                   
        source.expression += start;
        source.expression += (conditionBuilder as ConditionBuilderImpl<any>).expression;
        source.args = source.args.concat((conditionBuilder as ConditionBuilderImpl<any>).args);        
        source.expression += end;        
        this.conditionUtil = (conditionBuilder as ConditionBuilderImpl<any>).conditionUtil;
    }   

    then<TR = any, N = never>(
        onfulfilled?: (value: any) => TR | PromiseLike<TR>, 
        onrejected?: (reason: any) => N | PromiseLike<N> ) : Promise<TR | N> {
        return Object.getPrototypeOf(this.source).then.apply(this.source, arguments);
    }
    
    catch<N = never>(onrejected?: (reason: any) => N | PromiseLike<N>): Promise<N> {
        return Object.getPrototypeOf(this.source).catch.apply(this.source, arguments);
    }

    and( builder: (cu: CU)=> void): ConditionParenthesis<CU,P>;
    and( builder: ConditionBuilder<CU>): ConditionParenthesis<CU,P>;
    and( builder: any): ConditionParenthesis<CU,P>{   
        return this.appendConditionParenthesis(builder, ' and (', ') ');
    }

    or( builder: (cu: CU)=> void): ConditionParenthesis<CU,P>;
    or( builder: ConditionBuilder<CU>): ConditionParenthesis<CU,P>;
    or( builder: any): ConditionParenthesis<CU,P>{   
        return this.appendConditionParenthesis(builder, ' or (', ') ');
    }
    
    andNot( builder: (cu: CU)=> void): ConditionParenthesis<CU,P>;
    andNot( builder: ConditionBuilder<CU>): ConditionParenthesis<CU,P>;
    andNot( builder: any): ConditionParenthesis<CU,P>{   
        return this.appendConditionParenthesis(builder, ' and not (', ') ');
    }

    orNot( builder: (cu: CU)=> void): ConditionParenthesis<CU,P>;
    orNot( builder: ConditionBuilder<CU>): ConditionParenthesis<CU,P>;
    orNot( builder: any): ConditionParenthesis<CU,P>{   
        return this.appendConditionParenthesis(builder, ' or not (', ') ');
    }
    
    andExists( sql: string, ...args: any[]): ConditionParenthesis<CU,P>{
        let cu = this.conditionUtil.newInstance();
        return new ConditionParenthesis<CU,P>(this.source, ' and exists (', ') ', (cu as unknown as ConditionUtilImpl<CU>).conditionBuilder.append(sql, args));
    }

    andNotExists( sql: string, ...args: any[]): ConditionParenthesis<CU,P>{           
        let cu = this.conditionUtil.newInstance();
        return new ConditionParenthesis<CU,P>(this.source, ' and not exists (', ') ', (cu as unknown as ConditionUtilImpl<CU>).conditionBuilder.append(sql, args));
    }

    orExists( sql: string, ...args: any[]): ConditionParenthesis<CU,P>{           
        let cu = this.conditionUtil.newInstance();
        return new ConditionParenthesis<CU,P>(this.source, ' or exists (', ') ', (cu as unknown as ConditionUtilImpl<CU>).conditionBuilder.append(sql, args));
    }

    orNotExists( sql: string, ...args: any[]): ConditionParenthesis<CU,P>{           
        let cu = this.conditionUtil.newInstance();
        return new ConditionParenthesis<CU,P>(this.source, ' or not exists (', ') ', (cu as unknown as ConditionUtilImpl<CU>).conditionBuilder.append(sql, args));
    }    

    private appendConditionParenthesis(builder: any, start: string, end: string): ConditionParenthesis<CU,P>{   
        if(builder instanceof ConditionBuilderImpl){
            return new ConditionParenthesis<CU,P>(this.source, start, end, builder);
        }
        else{
            let cu = this.conditionUtil.newInstance();;
            builder(cu);
            return new ConditionParenthesis<CU,P>(this.source, start, end, (cu as unknown as ConditionUtilImpl<CU>).conditionBuilder);
        }
    }

    count(): Promise<any>{   
        this.source.isCount = true;
        return this.source as unknown as Promise<any>;
    }

    limit(limit:number, limitOffset?: number): Promise<any>{
        this.source.limitN = limit;
        this.source.limitOffset = limitOffset;
        return this.source as unknown as Promise<any>;
    }

    asParameters(): P{
        return (this.source as Statement<P>).asParameters();
    }
}

interface ConditionBuilder<CU>{
    append(segment: string, ...args: any[]): ConditionBuilder<CU>;

    and(): CU;

    or(): CU;  
}

class ConditionBuilderImpl<CU> implements ConditionSource, ConditionBuilder<CU>, Promise<any>{
    expression = '';    
    args:any[] = [];        
    conditionUtil: CU;
    isCount = false;
    limitN?: number;
    limitOffset?: number;

    private promise: Promise<any>;

    constructor(conditionUtil: CU){
        this.conditionUtil = conditionUtil;

        this.promise = new Promise( (resolver, reject) =>{
            this.doThen(resolver, reject);
        });
    }        

    [Symbol.toStringTag]: string;

    then<TR = any, N = never>(
        onfulfilled?: (value: any) => any, 
        onrejected?: (reason: any) => any) : Promise<any> {
        return this.promise.then(onfulfilled, onrejected);  
    }
    
    catch<N = never>(onrejected?: (reason: any) => N | PromiseLike<N>): Promise<N> {
        return this.promise.catch(onrejected);
    }

    finally(onfinally?: () => void): Promise<any> {
        return this.promise.finally(onfinally);
    }

    async doThen(onfulfilled?: (value: any) => any, onrejected?: (reason: any) => any ){ 
        try{
            onfulfilled(this.expression);
        }
        catch(error){
            onrejected(error);
        }         
    }

    append(segment: string, ...args: any[]): ConditionBuilder<CU>{
        if(args != null && Array.isArray(args[0])){
            args = args[0];
        }

        this.expression += ' ' + segment;
        this.args = this.args.concat(args);
        return this;
    }

    and(): CU{
        this.expression += ' and ';
        return this.conditionUtil;
    }

    or(): CU{
        this.expression += ' or ';
        return this.conditionUtil;
    }    
}

abstract class ConditionUtilImpl<CU> implements ConditionUtil<CU>{
    conditionBuilder: ConditionBuilderImpl<any>;
    
    constructor(){
        this.conditionBuilder = new ConditionBuilderImpl<any>(this);        
    }    

    newPar(builder: (cu: CU) => void): ConditionParenthesis<CU, any>;
    newPar(builder: ConditionBuilder<any>): ConditionParenthesis<CU, any>;
    newPar(builder): ConditionParenthesis<CU, any> {
        return this.appendConditionParenthesis(builder, ' (', ') ');
    }

    notPar(builder: (cu: CU) => void): ConditionParenthesis<CU, any>;
    notPar(builder: ConditionBuilder<any>): ConditionParenthesis<CU, any>;
    notPar(builder): ConditionParenthesis<CU, any> {
        return this.appendConditionParenthesis(builder, ' not (', ') ');
    }

    exists(sql: string, ...args: any[]){
        this.conditionBuilder.append(' exists (' + sql +') ', args);
    }    

    notExists(sql: string, ...args: any[]){
        this.conditionBuilder.append(' not exists (' + sql +') ', args);
    }    

    private appendConditionParenthesis(builder: any, start: string, end: string): ConditionParenthesis<CU, any>{
        if(builder instanceof ConditionBuilderImpl){
            return new ConditionParenthesis<CU, any>(this.conditionBuilder, start, end, builder);
        }
        else{
            let cu = this.newInstance();
            builder(cu);
            return new ConditionParenthesis<CU, any>(this.conditionBuilder, start, end,
                (cu as unknown as ConditionUtilImpl<any>).conditionBuilder as ConditionBuilderImpl<CU>);
        }
    }

    and(): CU{
        this.conditionBuilder.append(' and ');
        return this as unknown as CU;
    }

    or(): CU{
        this.conditionBuilder.append(' or ');
        return this as unknown as CU;
    }

    raw(expression: string, ...args: any[]) {
        this.conditionBuilder.append(expression, args);
        return this as unknown as CU;
    }

    abstract newInstance(): CU;
}

interface ConditionUtil<CU>{
    newPar(builder: (cu: CU)=> void ): ConditionParenthesis<CU, any>;
    newPar(builder: ConditionBuilder<any>): ConditionParenthesis<CU, any>;
    
    notPar(builder: (cu: CU)=> void ): ConditionParenthesis<CU, any>;
    notPar(builder: ConditionBuilder<any>): ConditionParenthesis<CU, any>;

    exists(sql: string, ...args: any[]);    
    notExists(sql: string, ...args: any[]);

    and(): CU;

    or(): CU;

    raw(expression: string, ... args:any[])
}



class FieldUtil<CU> extends FieldInfo{    
    tableField: string;
    private conditionUtil: CU;
    private conditionBuilder: ConditionBuilder<CU>;

    constructor(tableField: string, modelField: string, title: string, conditionUtil: CU){        
        super(modelField, title);
        this.tableField = tableField;
        this.conditionUtil = conditionUtil;
        this.conditionBuilder = (conditionUtil as unknown as ConditionUtilImpl<any>) .conditionBuilder;
    }    

    toString(): string{
        return this.tableField;
    }

    equals(value: any): ConditionBuilder<CU>{
        return this.conditionBuilder.append(this.tableField + ' = ? ', value);        
    }

    notEquals(value: any): ConditionBuilder<CU>{
        return this.conditionBuilder.append(this.tableField + ' <> ? ', value);        
    }

    moreThan(value: any): ConditionBuilder<CU>{
        return this.conditionBuilder.append(this.tableField + ' > ? ', value);        
    }

    moreThanOrEquals(value: any): ConditionBuilder<CU>{
        return this.conditionBuilder.append(this.tableField + ' >= ? ', value);        
    }

    lessThan(value: any): ConditionBuilder<CU>{
        return this.conditionBuilder.append(this.tableField + ' <? ', value);        
    }

    lessThanOrEqual(value: any): ConditionBuilder<CU>{
        return this.conditionBuilder.append(this.tableField + ' <= ? ', value);        
    }

    in(...values: any): ConditionBuilder<CU>{
        if(values && Array.isArray(values[0])){
            values = values[0];
        }

        let maskString = null;                
        for(let value in values){
            if(maskString == null){
                maskString = "?";                        
            }
            else{
                maskString += ",?";   
            }                              
        }
        return this.conditionBuilder.append(this.tableField + ' in (' + maskString + ')', values);        
    }

    notIn(...values: any): ConditionBuilder<CU>{
        let maskString = null;                
        for(let value in values){
            if(maskString != null){
                maskString += ",";                        
            }
            
            maskString += "?";                    
        }
        return this.conditionBuilder.append('not (' + this.tableField + ' in (' + maskString + '))', values);        
    }

    isNull(): ConditionBuilder<CU>{
        return this.conditionBuilder.append(this.tableField + ' is null ');        
    }

    notNull(): ConditionBuilder<CU>{
        return this.conditionBuilder.append('not (' + this.tableField + ' is null) ');        
    }

    between(from: any, to: any): ConditionBuilder<CU>{
        return this.conditionBuilder.append(this.tableField + ' between ? and ? ', from, to);        
    }

    notBetween(from: any, to: any): ConditionBuilder<CU>{
        return this.conditionBuilder.append('not (' + this.tableField + ' between ? and ?) ', from, to);          
    }

    like(expression: string, value?: any): ConditionBuilder<CU>{
        return this.conditionBuilder.append(this.tableField + ' like ? ' , expression);   
    }
}

interface TableUtil<CU, MI> extends ModelUtil<CU, MI>{
    tableName: string;
    tableFields: string[];
    modelIdFields: string[];
    tableIdFields: string[];
    idType: IdType;

    insert(data: MI): InsertStatement<CU, MI>;
    insert(data: MI[]): InsertStatement<CU, MI>;
    insert(data: any): InsertStatement<CU, MI>;

    update(data?: any): UpdateStatement<CU, MI>;
    updateByParameters(parameters: UpdateParameters<MI>): UpdateStatement<CU, MI>;

    delete(data?: any): DeleteStatement<CU, MI>;
    deleteByParameters(parameters: DeleteParameters<MI>): DeleteStatement<CU, MI>;    

    updateByState(data: any[]): UpdateByStateStatement;    
}

abstract class TableUtilImpl<CU, MI> extends ModelUtilImpl<CU,MI> implements TableUtil<CU, MI>{
    abstract tableName: string;
    abstract tableFields: string[];
    abstract modelIdFields: string[];
    abstract tableIdFields: string[];
    abstract idType: IdType;        
    selectSQL = null;
    joinTables: JoinTable[] = null;
    subTables: SubTable[] = null;        

    private createSelectByIdQuery(){
        let fieldsString = null;
        for(let i=0; i< this.tableFields.length; i++){
            if(i == 0){
                fieldsString = this.tableFields[i];
            }
            else{
                fieldsString += ',' + this.tableFields[i];
            }   
        }   

        let idFilter = null;
        for(let i=0; i<this.tableIdFields.length; i++){
            if(i == 0){
                idFilter = this.tableIdFields[i] + ' = ?';
            }
            else{
                idFilter += ' and ' + this.tableIdFields[i] + ' = ?';
            } 
        }

        return 'select ' + fieldsString + ' from ' + this.tableName + ' where ' + idFilter;   
    }

    async selectByIds(...args):Promise<any>{
        this.knex.raw('select * from ', args)
    }

    insert(data: MI): InsertStatement<CU, MI>;
    insert(data: MI[]): InsertStatement<CU, MI>;
    insert(data: any): InsertStatement<CU, MI>{
        return new InsertStatement(this, data, this.knex, false);
    }

    insertSelective(data: MI): InsertStatement<CU, MI>;
    insertSelective(data: MI[]): InsertStatement<CU, MI>;
    insertSelective(data: any): InsertStatement<CU, MI>{
        return new InsertStatement(this, data, this.knex, false, true);
    }

    insertOrUpdate(data: MI): InsertStatement<CU, MI>;
    insertOrUpdate(data: MI[]): InsertStatement<CU, MI>;
    insertOrUpdate(data: any): InsertStatement<CU, MI>{
        return new InsertStatement(this, data, this.knex, true);
    }

    update(data?: any): UpdateStatement<CU, MI>{
        return new UpdateStatement(this, data, this.knex);
    }

    updateSelective(data?: any): UpdateStatement<CU, MI>{
        return new UpdateStatement(this, data, this.knex, true);
    }

    updateByParameters(parameters: UpdateParameters<MI>): UpdateStatement<CU, MI>{
        let result = new UpdateStatement(this, null, this.knex);
        result.updateType = parameters.updateType;
        result.expression = parameters.where;
        result.data = parameters.data;
        result.args = parameters.args;
        return result;
    }

    delete(data?: any): DeleteStatement<CU, MI>{
        return new DeleteStatement(this.knex, this, data);
    }
    deleteByParameters(parameters: DeleteParameters<MI>): DeleteStatement<CU, MI>{
        let result = new DeleteStatement(this.knex, this);
        result.deleteType = parameters.deleteType;        
        result.args = parameters.args;
        result.data = parameters.data;
        result.expression = parameters.where;
        return result;
    }

    getRows(data: any[], tableUtilClass: Function):any[]{
        return this.getSubtableRows(data, this, tableUtilClass);
    }

    private getSubtableRows(data: any[], currentTableUtil: TableUtilImpl<any,any>, tableUtilClass: Function): any[]{
        if(currentTableUtil.constructor.name == tableUtilClass.name){
            return data;
        }

        let finalResult = [];

        if(this.subTables == null || this.subTables == undefined){
            return finalResult;
        }        

        for(let i=0;i<data.length; i++){
            for(let j=0; j<this.subTables.length; j++){
                let subtable = this.subTables[j] as SubTable;                
                let subtableUtil = this.subTables[j].getTableUtil() as TableUtilImpl<any,any>;
                let children = data[i][this.subTables[j].name];
                if(children != null && children != undefined){
                    let result = subtableUtil.getSubtableRows(children, subtableUtil, tableUtilClass);
                    if(result && result.length){
                        for(let k=0;k<subtable.foreignKeyFields.length;k++){
                            for(let l=0;l<result.length;l++){
                                result[l][subtable.foreignKeyFields[k].modelField] = data[i][subtable.parentIdFields[k].modelField];
                            }
                        }
                    }
                    if(result != null && result != undefined && result.length > 0){
                        finalResult = finalResult.concat(result);
                    }
                }                
            }
        }

        return finalResult;
    }

    getAllSubtables(): SubTable[]{
        let finalResult = [];
        if(this.subTables == null || this.subTables == undefined){
            return finalResult;
        }

        for(let i=0; i<this.subTables.length; i++){
            finalResult[finalResult.length] = this.subTables[i];
            finalResult = finalResult.concat((this.subTables[i].getTableUtil() as TableUtilImpl<any,any>).getAllSubtables());
        }

        return finalResult;
    }

    updateByState(data: any[]): UpdateByStateStatement{
        return new UpdateByStateStatement(this.knex, this, data);
    }
}

enum IdType{
    UUID = 1,
    AutoIncrement = 2,
    CUSTOM = 3
}

class InsertStatement<CU, MI> implements PromiseLike<any>{        
    private knex: Knex;      

    private data: any;
    private tableUtil: TableUtil<CU, MI>;
    private isWithSubtables = false;
    private isInsertOrUpdate = false;
    private subtablesMap = {};
    private isSelective = false;
   
    constructor(tableUtil: TableUtil<CU, MI>, data: any, knex: Knex, isInsertOrUpdate: boolean, selective: boolean = false){
        this.knex = knex;    
        this.tableUtil = tableUtil;             
        this.data = data;
        this.isInsertOrUpdate = isInsertOrUpdate;
        this.isSelective = selective;
    }

    withSubtables(...subtables: Function[]):InsertStatement<CU,MI>{
        if(subtables != null && subtables != undefined){
            if(Array.isArray(subtables[0])){
                subtables = subtables[0];
            }
            
            if(subtables.length > 0){                
                for(let i=0;i<subtables.length;i++){
                    if(subtables[i]){
                        this.isWithSubtables = true;
                        this.subtablesMap[subtables[i].name] = 1;
                    }
                }
            }
        }

        return this;
    }

    async then( onfulfilled?: (value: any) => any, onrejected?: (reason: any) => any ) : Promise<any>{ 
        if(this.tableUtil.idType == IdType.UUID){
            this.generateId();
        }                
        
        if(!Array.isArray(this.data)){
            this.data = [this.data];
        }    

        let sql:string = null;        
        let records = this.data as MI[];

        let tx:Knex.Transaction = Zone.current.get('tx');                
        if(this.isSelective){        
            let affectedRows = 0; 
            try{               
                for(let i=0; i<records.length; i++){    
                    let args = [];
                    sql = this.generateSelectiveInsertSQL(records[i], args)
                    let raw = this.knex.raw(sql, args);
                    if(tx != null && tx != undefined){
                        raw.transacting(tx);
                    }

                    let result = await raw.then();
                    affectedRows += result[0]['affectedRows'];
                }

                await this.insertSubtables();
                return onfulfilled(affectedRows);
            }
            catch(error){
                return onrejected(error)
            }
        }
        else{                
            let args = [];       
            sql = this.generateBatchInsertSQL(records, args);
            if(this.isInsertOrUpdate){
                sql += this.generateOnDuplicate();
            }

            let raw = this.knex.raw(sql, args);
            if(tx != null && tx != undefined){
                raw.transacting(tx);
            }
            return raw.then((value: any):any=>{           
                this.insertSubtables().then(()=>{           
                    onfulfilled(value[0]['affectedRows']);
                }, onrejected);
            }, onrejected);
        }                        
    }    

    generateFiels(record?: any): string{
        let fields = null;        
        for(let i=0; i< this.tableUtil.tableFields.length; i++){
            if(this.tableUtil.idType == IdType.AutoIncrement && this.tableUtil.tableIdFields[0] == this.tableUtil.tableFields[i]){
                continue;
            }

            if(this.isSelective && !record[this.tableUtil.modelFields[i]] && record[this.tableUtil.modelFields[i]] !== false){
                continue;
            }    

            if(fields != null){
                fields += ",";                
            }
            else{
                fields = "";
            }

            fields += this.tableUtil.tableFields[i];            
        }
        return fields;
    }

    generateBatchInsertSQL(records: MI[], args: any[]):string{
        let placeholders = null;
        let fields = this.generateFiels(); 
        for(let i=0; i<records.length; i++){                
            if(placeholders != null){
                placeholders += ",";
            }
            else{
                placeholders = "";
            }    
            
            placeholders += this.generateArgs(records[i], args);
        }
        
        return "insert into " + this.tableUtil.tableName + "(" + fields + ")" + " values " + placeholders;
    }

    generateSelectiveInsertSQL(record: MI, args: any[]):string{
        let fields = this.generateFiels(record); 
        return "insert into " + this.tableUtil.tableName + "(" + fields + ")" + " values " + this.generateArgs(record, args);
    }

    generateOnDuplicate(): string {
        let result = null;
        for(let i=0;i<this.tableUtil.tableFields.length; i++){
            if(result == null){
                result = ' ON DUPLICATE KEY UPDATE ' + this.tableUtil.tableFields[i] + 
                    ' = VALUES('  + this.tableUtil.tableFields[i] + ')';
            }
            else{
                result += ',' + this.tableUtil.tableFields[i] + ' = VALUES('  + this.tableUtil.tableFields[i] + ')';
            }
        }
        
        return result;
    }

    private async insertSubtables(){
        let tableUtil = this.tableUtil as TableUtilImpl<CU,MI>;
        if(this.isWithSubtables){
            let subtables = tableUtil.getAllSubtables();
            for(let i=0;i<subtables.length;i++){
                if(!subtables[i])
                    continue;

                let subtableUtil = subtables[i].getTableUtil() as TableUtilImpl<any,any>;
                if(this.subtablesMap[subtableUtil.constructor.name]){
                    if(this.isInsertOrUpdate){
                        await subtableUtil.insertOrUpdate(tableUtil.getRows(this.data, subtableUtil.constructor));
                    }
                    else{
                        if(this.isSelective){
                            await subtableUtil.insertSelective(tableUtil.getRows(this.data, subtableUtil.constructor));
                        }
                        else{
                            await subtableUtil.insert(tableUtil.getRows(this.data, subtableUtil.constructor));
                        }                        
                    }
                }
            }
        }
    }

    private generateId(){
        let field = this.tableUtil.modelIdFields[0];

        if(this.data != null && this.data != undefined){
            if(Array.isArray(this.data)){
                for(let i=0; i<this.data.length; i++){                    
                    if(this.data[i][field] == null || this.data[i][field] == undefined){
                        this.data[i][field] = v4().toString().replace(/[-]/mg, '');
                        this.setSubtablesFK(this.data[i], this.data[i][field]);
                    }
                }
            }
            else{
                if(this.data[field] == null 
                    || this.data[field] == undefined){                        
                        this.data[field] = v4().toString().replace(/[-]/mg, '');
                        this.setSubtablesFK(this.data, this.data[field]);
                }
            }
        }
    }

    private setSubtablesFK(record: any, id: string){
        let subtables = (this.tableUtil as TableUtilImpl<any,any>).subTables;
        if(subtables == null || subtables == undefined)
            return;

        for(let j=0; j<subtables.length; j++){
            let subtableUtil = subtables[j].getTableUtil() as TableUtilImpl<any,any>;
            let children = record[subtables[j].name];
            if(children != null && children != undefined){
                for(let k=0; k< children.length; k ++){
                    children[k][subtables[j].foreignKeyFields[0].modelField] = id;
                }
            }                
        }
    }

    private generateArgs(record: MI, args: any[]): string{
        let placeholders = null;
        for(let j=0; j < this.tableUtil.tableFields.length; j++){
            if(this.tableUtil.idType == IdType.AutoIncrement &&
                this.tableUtil.tableIdFields[0] == this.tableUtil.modelFields[j])
                continue;
            
            if(this.isSelective && !record[this.tableUtil.modelFields[j]] && record[this.tableUtil.modelFields[j]] !== false)
                continue;

            let value = record[this.tableUtil.modelFields[j]];
            args[args.length] = value == undefined?null: value;

            if(placeholders == null){
                placeholders = "(";
            }
            else{
                placeholders += ",";
            }    

            placeholders += "?";
        }
        placeholders += ")";

        return placeholders;
    }    
}

class UpdateStatement<CU, MI> implements Statement<UpdateParameters<MI>>, PromiseLike<any>{    
    type: 'update';

    private knex?: Knex;      

    data?: any;
    private tableUtil: TableUtil<CU, MI>;

    expression:string = null;
    args: any[] = [];
    isCount = false;    
    limitN?: number;
    limitOffset?: number;
    isSelective = false;

    setFields: string[] = [];    
    updateType: UpdateType;
    sets: UpdateSet[] = [];    

    constructor(tableUtil: TableUtil<CU, MI>, data?: any, knex?: Knex, selective: boolean = false){
        this.knex = knex;    
        this.tableUtil = tableUtil;             
        this.data = data;
        this.isSelective = selective;
        if(this.data == null){
            this.updateType = UpdateType.ByWhere;
            this.createExpression();
        }
        else{
            this.updateType = UpdateType.ByData;
        }        
    }

    originalSQL: string;
    whereType?: WhereType;

    then(onfulfilled?: (value: any) => any, onrejected?: (reason: any) => any ) : PromiseLike<any>{ 
        if(this.updateType == UpdateType.ByData){
            return this.updateByData().then(onfulfilled, onrejected);
        }
        else{
            return this.updateByWhere().then(onfulfilled, onrejected);
        }
    }        

    async updateByData(): Promise<any>{ 
        return new Promise(async (resolver, reject)=>{
            try{                
                let where = null;        
                for(let i=0;i<this.tableUtil.tableIdFields.length; i++){
                    if(where != null){
                        where += ' and ';
                    }
                    else{
                        where = ' ';
                    }

                    where += this.tableUtil.tableIdFields[i] + " = ?";            
                }      
                
                if(Array.isArray(this.data)){
                    let records = this.data as MI[];
                    let result = 0;

                    if(this.isSelective){                        
                        for(let index=0; index < records.length; index ++){
                            let fields = this.generateSetFields(records[index]);
                            result += await this.updateSingle(fields, where, records[index]);
                        }
                    }
                    else{                
                        let fields = this.generateSetFields();        
                        for(let index=0; index < records.length; index ++){                            
                            result += await this.updateSingle(fields, where, records[index]);
                        }
                    }

                    resolver(result);
                }
                else{
                    let fields = this.generateSetFields(this.data);
                    resolver(await this.updateSingle(fields, where, this.data as MI));
                }       
            }
            catch(error){
                reject(error);
            }
        })        
    }

    private generateSetFields(record?: any): string{
        let fields = null;                
        for(let i=0; i< this.tableUtil.tableFields.length; i++){            
            if(this.isSelective && !(record[this.tableUtil.modelFields[i]]) && record[this.tableUtil.modelFields[i]]!== false){
                continue;
            }

            if(fields != null){
                fields += ",";                
            }
            else{
                fields = "";
            }

            fields += this.tableUtil.tableFields[i] + " = ?";                  
        }

        return fields;
    }

    private async updateSingle(fields: string, where: string, record: MI): Promise<any>{
        let args = [];
        for(let i=0; i< this.tableUtil.tableFields.length; i++){            
            let value = record[this.tableUtil.modelFields[i]];
            if(this.isSelective && !value && value !== false){
                continue;
            }

            args[args.length] = value == undefined? null: value;
        }
        for(let i=0; i< this.tableUtil.modelIdFields.length; i++){            
            args[args.length] = record[this.tableUtil.modelIdFields[i]];
        }        

        let tx:Knex.Transaction = Zone.current.get('tx');
        let raw = this.knex.raw("update " + this.tableUtil.tableName + " set " + fields + 
            " where " + where, args);
        if(tx != null && tx != undefined){
            raw.transacting(tx);
        }    
        return raw;
    }

    updateByWhere() : Promise<any>{ 
        return new Promise((resolver, reject)=>{
            let fields = null;                
            for(let i=0; i< this.setFields.length; i++){
                if(fields != null){
                    fields += ",";                
                }
                else{
                    fields = " set ";
                }

                fields += this.setFields[i] + " = ?";                  
            }        

            let tx:Knex.Transaction = Zone.current.get('tx');
            let raw = this.knex.raw(this.expression.replace(' set ', fields), this.args);
            if(tx != null && tx != undefined){
                raw.transacting(tx);
            }    
            raw.then((value: any):any=>{                      
                resolver(value[0]['affectedRows']);
            }, reject);
        })        
    }    

    protected createExpression(){
        this.expression = "update " + this.tableUtil.tableName + " set  where ";
    }

    set(field: any, value?: any): UpdateStatement<CU, MI> {
        this.setFields.push(field.toString());
        this.args.push(value);
        return this;
    }

    where(builder: (cu: CU)=> void ): ConditionParenthesis<CU, UpdateParameters<MI>>;
    where(builder: ConditionBuilder<any>): ConditionParenthesis<CU, UpdateParameters<MI>>;
    where(builder: string, ...args: any[]): ConditionParenthesis<CU, UpdateParameters<MI>>;
    where(builder, ...args: any[]): ConditionParenthesis<CU, UpdateParameters<MI>>{              
        if(builder instanceof ConditionBuilderImpl){
            return new ConditionParenthesis<CU, UpdateParameters<MI>>(this, '(', ') ', builder);
        }
        else if(typeof builder === "string" ){
            let cu = this.tableUtil.createConditionUtil();            
            return new ConditionParenthesis<CU, UpdateParameters<MI>>(this, '(', ') ', 
                (cu as unknown as ConditionUtilImpl<CU>).conditionBuilder.append(builder, args));            
        }
        else{
            let cu = this.tableUtil.createConditionUtil();
            builder(cu);
            return new ConditionParenthesis<CU, UpdateParameters<MI>>(this, '(', ') ',
                (cu as unknown as ConditionUtilImpl<any>).conditionBuilder as ConditionBuilderImpl<CU>);
        }
    }

    asParameters(): UpdateParameters<MI>{
        let result = {} as UpdateParameters<MI>;
        result.data = this.data;
        result.sets = this.sets;
        result.args = this.args;
        result.where = this.expression;
        return null;
    }
}

class DeleteStatement<CU, MI> implements Statement<DeleteParameters<MI>>, PromiseLike<any>{        
    type: "delete";
    private knex: Knex;      

    data?: any;
    private tableUtil: TableUtil<CU, MI>;

    expression:string = null;
    args: any[] = [];
    isCount = false;    
    limitN?: number;
    limitOffset?: number;

    private setFields: FieldUtil<any>[] = [];    
    deleteType?: DeleteType;

    private isWithSubtables = false;
    private subtablesMap = {};    

    constructor(knex: Knex, tableUtil: TableUtil<CU, MI>, data?: any){
        this.knex = knex;    
        this.tableUtil = tableUtil;             
        this.data = data;
        if(this.data == null){
            this.createExpression();
            this.deleteType = DeleteType.ByData;
        }
        else{
            this.deleteType = DeleteType.ByWhere;
        }        
    }    
    
    withSubtables(...subtables: Function[]):DeleteStatement<CU,MI>{
        if(subtables != null && subtables != undefined){
            if(Array.isArray(subtables[0])){
                subtables = subtables[0];
            }
            
            if(subtables.length > 0){                
                for(let i=0;i<subtables.length;i++){
                    if(subtables[i]){
                        this.isWithSubtables = true;
                        this.subtablesMap[subtables[i].name] = 1;
                    }
                }
            }
        }

        return this;
    }    

    then(onfulfilled?: (value: any) => any, onrejected?: (reason: any) => any ) : PromiseLike<any>{ 
        if(this.data != null){
            return this.deleteByData().then(onfulfilled, onrejected);
        }
        else{
            return this.deleteByWhere().then(onfulfilled, onrejected);
        }        
    }    
    
    async deleteByData() : Promise<any>{
        let condition = null;        
        for(let i=0;i<this.tableUtil.tableIdFields.length; i++){
            if(condition != null){
                condition += ' and ';
            }
            else{
                condition = ' ';
            }

            condition += this.tableUtil.tableIdFields[i] + " = ?";            
        }      
        
        let rows = null;
        if(Array.isArray(this.data)){
            rows = this.data as MI[];            
        }
        else{
            rows = [this.data as MI];
        }

        let result = this.deleteRows(condition, rows);        
        let tableUtil = this.tableUtil as TableUtilImpl<CU,MI>;
        if(this.isWithSubtables){
            let subtables = tableUtil.getAllSubtables();
            for(let i=0;i<subtables.length;i++){
                if(!subtables[i])
                    continue;

                let subtableUtil = subtables[i].getTableUtil() as TableUtilImpl<any,any>;
                if(this.subtablesMap[subtableUtil.constructor.name]){
                    await subtableUtil.delete(tableUtil.getRows(rows, subtableUtil.constructor));
                }
            }
        }

        return result;
    }

    private async deleteRows(condition: string, records: MI[]) : Promise<any>{       
        let args = [];
        let where = null;
        for(let i=0;i<records.length; i++){
            if(where == null){
                where = '(' + condition + ')';
            }
            else{
                where += ' or (' + condition + ')';
            }

            let record = records[i]
            for(let j=0;j<this.tableUtil.modelIdFields.length; j++){                           
                args[args.length] = record[this.tableUtil.modelIdFields[j]];
            }
        }
        
        let tx:Knex.Transaction = Zone.current.get('tx');
        let raw = this.knex.raw("delete from " + this.tableUtil.tableName + 
            " where " + where , args);
        if(tx != null && tx != undefined){
            raw.transacting(tx);
        }
        return raw;
    }

    async deleteByWhere() : Promise<any>{ 
        return new Promise((resolver, reject) =>{
            let fields = null;                
            for(let i=0; i< this.setFields.length; i++){
                if(fields != null){
                    fields += ",";                
                }
                else{
                    fields = " set ";
                }

                fields += this.setFields[i].tableField + " = ?";                  
            }        

            let tx:Knex.Transaction = Zone.current.get('tx');
            let raw = this.knex.raw(this.expression, this.args);
            if(tx != null && tx != undefined){
                raw.transacting(tx);
            }    
            
            raw.then((value: any):any=>{                      
                resolver(value[0]['affectedRows']);
            }, reject);
        })            
    }        

    protected createExpression(){
        this.expression = "delete from " + this.tableUtil.tableName + "  where ";
    }

    where(builder: (cu: CU)=> void ): ConditionParenthesis<CU, DeleteParameters<MI>>;
    where(builder: ConditionBuilder<any>): ConditionParenthesis<CU, DeleteParameters<MI>>;
    where(builder: string, ...args: any[]): ConditionParenthesis<CU, DeleteParameters<MI>>;
    where(builder, ...args: any[]): ConditionParenthesis<CU, DeleteParameters<MI>>{              
        if(builder instanceof ConditionBuilderImpl){
            return new ConditionParenthesis<CU, DeleteParameters<MI>>(this, '(', ') ', builder);
        }
        else if(typeof builder === "string" ){
            let cu = this.tableUtil.createConditionUtil();            
            return new ConditionParenthesis<CU, DeleteParameters<MI>>(this, '(', ') ', 
                (cu as unknown as ConditionUtilImpl<CU>).conditionBuilder.append(builder, args));            
        }
        else{
            let cu = this.tableUtil.createConditionUtil();
            builder(cu);
            return new ConditionParenthesis<CU, DeleteParameters<MI>>(this, '(', ') ',
                (cu as unknown as ConditionUtilImpl<any>).conditionBuilder as ConditionBuilderImpl<CU>);
        }
    }

    asParameters(): DeleteParameters<MI>{
        let result = {} as DeleteParameters<MI>;
        result.deleteType = this.deleteType;
        result.data = this.data;
        result.where = this.expression;
        result.args = this.args;
        return result;
    }    
}

class UpdateByStateStatement implements PromiseLike<any>{
    private knex?: Knex;    
    data: any[];
    private tableUtil: TableUtilImpl<any, any>;    
    private isWithSubtables = false;
    private subtablesMap = {};
    private withSubtablesParams: Function[] = null;    

    constructor(knex: Knex, tableUtil: TableUtilImpl<any,any>, data: any[]){
        this.knex = knex;    
        this.tableUtil = tableUtil;             
        this.data = data;                
    }    

    withSubtables(...subtables: Function[]):UpdateByStateStatement{
        if(subtables != null && subtables != undefined){
            if(Array.isArray(subtables[0])){
                subtables = subtables[0];
            }
            
            if(subtables.length > 0){                
                this.withSubtablesParams = subtables;
                for(let i=0;i<subtables.length;i++){
                    if(subtables[i]){
                        this.isWithSubtables = true;
                        this.subtablesMap[subtables[i].name] = 1;
                    }
                }
            }
        }

        return this;
    }

    then(onfulfilled?: (value: any) => any, onrejected?: (reason: any) => any ) : PromiseLike<any>{ 
        return this.doUpdate().then(onfulfilled, onrejected);                        
    }    

    async doUpdate(): Promise<any>{ 
        return new Promise(async (resolver, reject)=>{
            try{
                let effected = 0;
    
                let rows = this.getRowsByState(DataState.New);            
                if(rows.length > 0){
                    effected += await this.tableUtil.insert(rows);
                }
    
                rows = this.getRowsByState(DataState.Deleted);            
                if(rows.length > 0){
                    effected += await this.tableUtil.delete(rows).withSubtables(this.withSubtablesParams as any);
                }
    
                rows = this.getRowsByState(DataState.Changed);            
                if(rows.length > 0){
                    effected += await this.tableUtil.update(rows);
                }
                
                if(this.isWithSubtables && this.tableUtil.subTables){     
                    for(let i=0;i<this.tableUtil.subTables.length;i++){
                        let subtableUtil = this.tableUtil.subTables[i].getTableUtil() as TableUtilImpl<any,any>;
                        if(this.subtablesMap[subtableUtil.constructor.name]){
                            let subRows = this.tableUtil.getRows(this.data, subtableUtil.constructor);
                            if(subRows && subRows.length){
                                await subtableUtil.updateByState(subRows).withSubtables(this.withSubtablesParams as any);
                            }
                        }
                    }
                }
    
                resolver(effected);                
            }
            catch(error){
                reject(error);
            }            
        })
    }

    private getRowsByState(state: DataState): any[]{
        let result = [];
        for(let i=0;i<this.data.length; i++){
            if(this.data[i]._state == state){
                result[result.length] = this.data[i];
            }
        }
        return result;
    }
}

function Transactional(target: Object,
    propertyName: string,
    descriptor: TypedPropertyDescriptor<(...args: any[]) => any> ) {
    const method = descriptor.value;
    descriptor.value = async function(...args: any[]) {
        let currentZone = Zone.current;
        let newZone:Zone = null;
        
        if(currentZone.get('tx') == null || currentZone.get('tx') == undefined){            
            let knex:Knex = Container.get(Knex);
            let tx = await knex.transaction();            
            newZone = currentZone.fork( { name: v4().toString().replace(/[-]/mg, ''),
                properties: { 'tx': tx, 'txRoot': true } });
        }
        else{
            newZone = currentZone.fork({ 
                name: v4().toString().replace(/[-]/mg, ''), 
                properties: { 'txRoot': false } });
        }
        
        let object = this;        
     
        return new Promise((resolver, reject)=>{
            newZone.run(async ()=>{              
                try{  
                    let result = await method.apply(object, args);
                    if(Zone.current.get('txRoot')){
                        console.log('Do commit transaction');
                        let tx:Knex.Transaction = Zone.current.get('tx');
                        try{
                            tx.commit();                
                            resolver(result);
                        }
                        catch(error){
                            try{
                                console.log(error);
                                tx.rollback();        
                                reject(error);                        
                            }
                            catch(err){
                                console.log(err);
                                reject(err);
                            }
                        }
                    }
                }
                catch(error){
                    console.log(error);

                    if(Zone.current.get('txRoot')){
                        console.log('Do rollback transaction');
                        let tx:Knex.Transaction = Zone.current.get('tx');

                        try{
                            tx.rollback();                
                        }
                        catch(err){
                            console.log(err);
                        }
                    }    
                    reject(error);
                }                
            });
        });       
    };
}

enum ParametersType{
    Select = 1,
    Insert = 2,
    Update = 3,
    Delete = 4
}

interface Parameters{
    type: ParametersType;
}

abstract class SelectParameters implements Parameters{
    type: ParametersType.Select;
    abstract fields?: FieldUtil<any>[];
    abstract where?: string;
    abstract whereType?: WhereType;    
    abstract args?: any[];
    abstract isCount: boolean;    
    abstract limit: {
        max: number;
        offset?: number;
    }
}

abstract class InsertParameters<Model> implements Parameters{
    type: ParametersType.Insert;
    data: Model[];
}

enum UpdateType{
    ByData = 1,
    ByWhere = 2
}

abstract class UpdateParameters<Model> implements Parameters{
    type: ParametersType.Update;
    updateType: UpdateType;
    data?: Model[];
    sets?: UpdateSet[];
    where?: string;
    args?: any[];
}

interface UpdateSet{
    field: string;
    value: any;
}

enum DeleteType{
    ByData = 1,
    ByWhere = 2
}

abstract class DeleteParameters<Model> implements Parameters{
    type: ParametersType.Delete;
    deleteType?: DeleteType;
    data?: Model[];
    where?: string;
    args?: any[];
}

abstract class BasicModelService<Model>{
    private modelUtil: ModelUtil<any, Model>;
    private tableUtil: TableUtil<any, Model>;

    constructor(modelUtil: ModelUtil<any, Model>){
        this.modelUtil = modelUtil;
        if(modelUtil instanceof TableUtilImpl){
            this.tableUtil = modelUtil as TableUtilImpl<any,Model>;
        }
    }

    async service(parameters: Parameters): Promise<any>{
        switch(parameters.type){
            case ParametersType.Insert:{
                return this.insert(parameters as InsertParameters<any>);
            }
            case ParametersType.Update:{
                return this.update(parameters as UpdateParameters<any>);
            }            
            case ParametersType.Delete:{
                return this.delete(parameters as DeleteParameters<any>);
            }
            default: {
                return this.select(parameters as SelectParameters);
            }
        }
    }

    async select(parameters: SelectParameters): Promise<any>{        
        let select = this.modelUtil.select();
       if(parameters.where != null){
           select.where(parameters.where);
       }
       select.args = parameters.args;
       if(parameters.isCount){
           return select.count();
       }
       else{
           return select.limit(parameters.limit.max, parameters.limit.offset);
       }
    }

    async insert(parameters: InsertParameters<Model>): Promise<any>{
        return this.tableUtil.insert(parameters.data);
    }

    async update(parameters: UpdateParameters<Model>): Promise<any>{
        return this.tableUtil.updateByParameters(parameters);
    }

    async delete(parameters: DeleteParameters<Model>): Promise<any>{
        return this.tableUtil.deleteByParameters(parameters);
    }
}

interface GeneratedField{
    fieldUtil: FieldInfo;    
    alias: string;    
}

interface JoinTable{
    joinByDefault: boolean;
    foreignKeyFields: FieldUtil<any>[];        
    joinIdFields: FieldUtil<any>[]; 
    getTableUtil(): TableUtil<any,any>;
    fields: GeneratedField[];
}

interface SubTable{    
    name: string;
    parentIdFields: FieldUtil<any>[];        
    foreignKeyFields: FieldUtil<any>[]; 
    getTableUtil(): TableUtil<any,any>;    
}



export {ModelUtil, ModelUtilImpl, SelectStatement, ConditionParenthesis, ConditionBuilder, ConditionBuilderImpl,
     ConditionUtil, ConditionUtilImpl, FieldUtil, TableUtil, TableUtilImpl, Transactional, 
     BasicModelService, Parameters, IdType, JoinTable, SubTable}