/* Copyright 2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.kromhouts

/**
 * Provides a DSL for building and managing HQL strings.
 * For more usage examples see the HqlBuilderTests.
 * HQL reference see http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/queryhql.html
 *
 * DML reference see http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/batch.html#batch-direct
 * DML-style clauses: "update, delete, insert into" may not be well tested, the "set" clause has basic implementation.
 * So double check the q.query output for these cases.
 *
 * Primary goals:
 * 1. Easy to read and understand in code.
 * 2. Easy to read and understand when printed (e.g when displayed in a report).
 * 3. Easy to execute with correct paginateParams and namedParams.
 * 4. Easy to change a clause and execute again.
 *
 * Basic usage:
 *    def q = new HqlBuilder().query {
 *        select 'count(distinct book)'
 *        from 'Book as book'
 *        where 'book.id > 100'
 *            and 'book.inStock = true'
 *    }
 *    def totalCount = Book.executeQuery(q.query, q.namedParams, q.paginateParams)[0].toInteger()
 *    q.select = 'distinct book'
 *    def list = Book.executeQuery(q.query, q.namedParams, q.paginateParams)
 *
 *    def bookList = new PagedResultList(list, totalCount)
 *    log.debug '\n' + q.printFormattedQuery
 *
 * @author Gavin Kromhout
 * @version DraftB
 *
 */
class HqlBuilder {

    // HQL clauses.
    // Each clause is a map key with a list of terms.
    def clauses = [:]

    // HQL namedParams.
    // HQL requires the namedParams to match exactly with the clause expressions.
    def namedParams = [:]

    // HQL paginateParams.
    def paginateParams = [max: 1000, offset: 0]

    // The where clause terms are handled separately from other clauses
    // and are a list of logicalTerms.
    // The where clause is built by buildWhereClause.
    def whereClauseTerms = []

    // LogicalIndexStack holds indexes of the current whereClauseTerm nesting.
    def logicalIndexStack = []

   def logicalBuilders = [AND: 'and',
                                        OR: 'or']

    def nestingStack = []

    // Sort and Order.
    // It is easier and more flexible to simply add order as a clause, e.g: order 'by name desc, id asc'
    // def sort = "" // e.g. instanceName.id
    // def order = "" // e.g. asc or desc

    /**
     * Constructor.
     * Any property that exists (or responds) in the class may be supplied as an argument.
     * E.g: max:20, offset:10, debug:true
     * The debug property does not really exist, but if true and no external log property
     * has been setup then the internal mockLogger will be configured in debug mode.
     *
     * @param args A map of arguments, defaults to an empty map.
     *
     */
    def HqlBuilder(Map args = [:]) {
        args.each { arg ->
            def argKey = arg.key.toLowerCase()
            if(super.hasProperty(argKey))
                this[argKey] = arg.value
        }
        if(!super.metaClass.hasMetaProperty('log'))
            mockLogging(args.debug)
        log.debug "HqlBuilder()"
    }

    /**
     * Call with no args.
     *  Has no real use other than to prevent obscure errors.
     */
    def call() {
        log.debug "call()"
    }

    /**
     * Call with closure as last arg.
     * A typically used build call, e.g: q { } is equivalent to q.call() { }
     */
    def call(Closure cl) {
        log.debug "call(Closure cl)"
        handleClosure(cl)
    }

    /**
     * Domain specific build method.
     *  Has no real use other than to prevent obscure errors
     * when user makes a call to query() and Groovy calls query(Closure cl)
     *
     * @returns This object.
     *
     */
    def query() {
        log.debug "query()"
        return this // Must return this object to q.
    }

    /**
     * Domain specific build method.
     * The recommended build call, e.g: def q = new HqlBuilder().query { }
     *
     * @param cl The closure that will be used to build the query.
     * @returns This object.
     *
     */
    def query(Closure cl) {
        log.debug "query(Closure cl)"
        handleClosure(cl)
        return this // Must return this object to q.
    }

    /**
     * InvokeMethod resolves all undefined methods.
     * Which include the clause methods, e.g: select 'book' is equivalent to select('book').
     * Note that defined methods will be called directly since this class does not implement GroovyInterceptable.
     * If class was "HqlBuilder implements GroovyInterceptable" then even println would be intercepted and
     * several exlusions might be needed. e.g: if(methodName != 'call' && methodName != 'println')
     */
    def invokeMethod(String methodName, args) {

        log.debug "invokeMethod(${methodName}, ${args})"

        // Call any closures first, that way the nesting is handled and we just keep a reference.
        if(args[-1] instanceof Closure) {
            handleClosure(args[-1], methodName)
            args = args.minus(args[-1])
        }

        if(!clauses.containsKey(methodName) && !isLogicalBuilder(methodName))
            clauses[methodName] = []

        if(args) {
            if(isWhereClauseBuilder(methodName)) {
                logicalBuilder(methodName, args)
                return
            }
        }

        for(arg in args) {
            if(arg instanceof String || arg instanceof GString)
                clauses[methodName] << arg
        }

    } // invokeMethod()

    /**
     * PropertyMissing.
     * Allows clauses to be added after build, e.g: q.order = 'by book.name asc'
     * and clauses to be removed, e.g: q.order = null
     */
    def propertyMissing(String propertyName, value) {
        log.debug "propertyMissing(${propertyName}, ${value})"

        if(value == null) {
            removeClause(propertyName)
            if(propertyName.toLowerCase() == 'where')
                whereClauseTerms.clear()
            return
        }

        if(!clauses.containsKey(propertyName))
            clauses[propertyName] = []

        // Occurs when user assigns to where clause, e.g: q.where = 'book.id > 100'
        if(propertyName.toLowerCase() == 'where') {
            whereClauseTerms.clear()
            logicalBuilder(propertyName, [value])
            return
        }

        if(value instanceof String || value instanceof GString)
            clauses[propertyName] = [value]
    } // propertyMissing(String propertyName, value)

    /**
     * PropertyMissing.
     * Allow clauses to be accessed directly by name, e.g: println q.order.
     * Since clauses is a Map null is simply returned for a non-existant clause.
     */
    def propertyMissing(String propertyName) {
        log.debug "propertyMissing(${propertyName})"

        if(!clauses.containsKey(propertyName))
            clauses[propertyName] = []

        // Occurs when user performs an operation on where clause.
        // E.g: q.where << "book.id = 100" which is actually NOT a supported operation since
        // calling the method provides the correct function e.g: q.where "book.id > 100".
        // Also allows `println q.where` to be short hand for `println q.whereClauseTerms`
        if(propertyName.toLowerCase() == 'where') {
            return whereClauseTerms
        }

        clauses[propertyName]
    } // propertyMissing(String propertyName)

    def setMax(Integer value) {
        paginateParams.max = value
    }

    def getMax() {
        paginateParams.max
    }

    def setOffset(Integer value) {
        paginateParams.offset = value
    }

    def getOffset() {
        paginateParams.offset
    }

    /**
     * RemoveClause.
     * Allows clauses to be removed, e.g: q.removeClause('order')
     *
     * @param clauseName The clause to remove.
     *
     */
    def removeClause(String clauseName) {
            clauses.remove(clauseName)
    }

    /**
     * BuildWhereClause.
     * Build the where clause from whereClauseTerms.
     */
    def buildWhereClause(printFormat = false) {
        //log.debug "buildWhereClause()"

        if(!whereClauseTerms)
            return ''

        def whereClause = 'where '

        def buildExpression // declared separately to allow recurrsion.
        buildExpression = { term ->
            def result = ''
            def termCount = term.expressions.size()
            if(termCount > 1) {
                term.expressions.eachWithIndex { t, index ->
                    if(index == 0)
                        result += buildExpression(t)
                    else if(printFormat)
                        result += " \n\t${t.logic} ${buildExpression(t)}"
                    else
                        result += " ${t.logic} ${buildExpression(t)}"

                }
                result = "( "+result+" )"
            }
            else {
                if(term.expressions[0] instanceof Map)
                    result += "${term.expressions[0].expressions[0]}"
                else
                    result += "${term.expressions[0]}"
            }
            return result
        }

        whereClauseTerms.eachWithIndex { tm, index ->
            if(index == 0)
                whereClause += buildExpression(tm)
            else if(printFormat)
                whereClause += " \n\t${tm.logic} ${buildExpression(tm)}"
            else
                whereClause += " ${tm.logic} ${buildExpression(tm)}"
        }

        return whereClause
    } // buildWhereClause(printFormat = false)

    /**
     * LogicalBuilder.
     * Build the whereClauseTerms
     * by appending logicalTerms to the appropriate expressions.
     */
    def logicalBuilder(logicalName, args) {
        log.debug "logicalBuilder(${logicalName}, ${args})"
        log.debug "logicalIndexStack: ${logicalIndexStack}"

        def logic = getLogicalString(logicalName)

        for(arg in args) {
            if(arg instanceof String || arg instanceof GString) {
                arg = arg.trim()
                if(arg) { // prevent empty strings being added.
                    if(logicalIndexStack.size() > 0) {
                        // Append to current index position.
                        whereClauseTerms[logicalIndexStack[-1]].expressions << logicalTerm(logic, arg)
                    }
                    else {
                        // Append to 'root'.
                        whereClauseTerms << logicalTerm(logic, null) // empty expression logicalTerm.
                        whereClauseTerms[-1].expressions << logicalTerm(logic, arg) // append logicalTerm to expressions
                    }
                } // if(arg)
            } // if(arg instanceof)
        } // for

    } // logicalBuilder(logicalName, args)

    /**
     * LogicalTerm.
     * A logicalTerm is a map object that holds the logic and list of expressions of a whereClauseTerm.
     */
    def logicalTerm = { logic, expression ->
        expression = expression ? [expression] : []
        ['logic': getLogicalString(logic), 'expressions': expression]
    }

    /**
     * GetLogicalString.
     *
     * @param logicalName The name to get the matching logicalBuilder string for.
     */
    private getLogicalString(logicalName) {

        switch(logicalName.toLowerCase()) {
            case 'where':
                logicalBuilders.AND
                break
            case logicalBuilders.AND:
                logicalBuilders.AND
                break
            case logicalBuilders.OR:
                logicalBuilders.OR
                break
        }

    }

    /**
     * HandleClosure.
     * Setting delegate and DELEGATE_FIRST allows closure to access this object's properties first.
     */
    private handleClosure(Closure cl, String methodName = 'root') {
        log.debug "handleClosure(${cl.toString()}, ${methodName})"
        if(isWhereClauseBuilder(methodName)) {
            whereClauseTerms << logicalTerm(getLogicalString(methodName), null)
            logicalIndexStack << whereClauseTerms.size()-1
        }
        nestingStack.push(methodName)
        cl.delegate = this
        cl.resolveStrategy = Closure.DELEGATE_FIRST
        cl.call()
        //log.debug "nestingStack: $nestingStack"
        nestingStack.pop()
        if(isWhereClauseBuilder(methodName)) {
            logicalIndexStack.pop()
        }
    }

    /**
     * MockLogging.
     * This class has super cow powers and can mock out it's own debug logging.
     */
    private mockLogging(debug = false) {
        def mockLogger = {}
        if(debug) {
            mockLogger = {msg ->
                    println "${super.getClass()} - DEBUG: $msg"
            }
        }
        super.metaClass.log = [debug: mockLogger]
        log.debug "Internal mockLogger configured."
    }

    /**
     * IsLogicalBuilder.
     * Determine if a method is a logicalBuilder.
     */
    private isLogicalBuilder(String methodName) {
        logicalBuilders.find{ it.value == methodName.toLowerCase()} ? true:false
    }

    /**
     * IsWhereClauseBuilder.
     * Determine if a method is a where clause builder.
     */
    private isWhereClauseBuilder(String methodName) {
        methodName = methodName.toLowerCase()
        if(methodName == 'where' || isLogicalBuilder(methodName))
            return true
        else
            return false
    }

    /**
     * GetQuery.
     * Assemble and return the query in a format that can be directly executed.
     * E.g: executeQuery(q.query, q.namedParams, q.paginateParams).
     */
    def getQuery() {
        clauses.collect { clause ->
            switch (clause.key.toLowerCase()) {
                case 'select':
                    clause.key + ' ' + clause.value.join(', ')
                    break
                case 'set':
                    clause.key + ' ' + clause.value.join(', ')
                    break
                case 'where':
                    buildWhereClause()
                    break
                case 'order':
                    clause.key + ' ' + clause.value.join(', ')
                    break
                case 'group':
                    clause.key + ' ' + clause.value.join(', ')
                    break
                default:
                    clause.key + ' ' + clause.value.join(' ')
            }
        }.join(' ')
    } // getQuery()

    /**
     * GetPrintFormattedQuery.
     * Assemble and return the query in a format that can be more easily printed and read by a person.
     * E.g: println q.printFormattedQuery or when displayed in a report.
     */
    def getPrintFormattedQuery() {
        clauses.collect { clause ->
            switch (clause.key.toLowerCase()) {
                case 'select':
                    clause.key + ' ' + clause.value.join(', \n\t')
                    break
                case 'set':
                    clause.key + ' ' + clause.value.join(', \n\t')
                    break
                case 'where':
                    buildWhereClause(true)
                    break
                case 'order':
                    clause.key + ' ' + clause.value.join(', \n\t')
                    break
                case 'group':
                    clause.key + ' ' + clause.value.join(', \n\t')
                    break
                default:
                    clause.key + ' ' + clause.value.join(' \n\t')
            }
        }.join(' \n')
    } // getPrintFormattedQuery()

} // end class
