import net.kromhouts.HqlBuilder
import grails.orm.PagedResultList
import org.hibernate.FetchMode as FM

/**
* Service class that encapsulates the business logic for Task searches.
*/
class TaskSearchService {

    boolean transactional = false

    def authService
    def dateUtilService
    def messageSource

    def g = new org.codehaus.groovy.grails.plugins.web.taglib.ApplicationTagLib()

    def paramsMax = 100000

    // Be sure to update taskQuickSearchPane.js if quickSearchSelection is changed.
    def getQuickSearchSelection() {
        [ 'allTasks': g.message(code: 'task.search.text.all.tasks'),
        'budgetPlanned': g.message(code: 'task.search.text.budget.planned'),
        'budgetUnplanned': g.message(code: 'task.search.text.budget.unplanned'),
        'personsImmediateCallouts': g.message(code: 'task.search.text.persons.immediate.callouts'),
        'personsTasks': g.message(code: 'task.search.text.persons.tasks')
        ]
    }

    /**
    * Selects and returns the correct search results based on the supplied quickSearch.
    * @param params The request params, may contain params.quickSearch string to specify the search.
    * @param locale The locale to use when generating result.message.
    */
    def getQuickSearch(params = [:], locale) {
        def result = [:]
        result.quickSearch = params.quickSearch ?: "personsTasks"
        if(params.person)
            result.person = Person.get(params.person.id.toLong())
        else
            result.person = authService.currentUser
        result.startDate = params.startDate ?: dateUtilService.today
        result.endDate = params.endDate ?: dateUtilService.today
        // Auto swap date range.
        if(result.startDate > result.endDate) {
            def tempStartDate = result.startDate
            result.startDate = result.endDate
            result.endDate = tempStartDate
        }
        result.includeCompleted = params.includeCompleted = params.includeCompleted ? true:false

        def getMessage = { Map m ->
            messageSource.getMessage(m.code, m.args == null ? null : m.args.toArray(), locale)
        }

        def formatted = { Date d ->
            g.formatDate(format: "EEE, dd-MMM-yyyy", date: d)
        }

        def startOfToday = dateUtilService.today
        def startOfYesterday = dateUtilService.yesterday
        def startOfTomorrow = dateUtilService.tomorrow
        def oneWeekAgo = dateUtilService.oneWeekAgo

        def formattedStartOfToday = formatted(startOfToday)
        def formattedStartOfYesterday = formatted(startOfYesterday)
        def formattedStartOfTomorrow = formatted(startOfTomorrow)
        def formattedOneWeekAgo = formatted(oneWeekAgo)

        def allTasks = {
            result.taskInstanceList = getTasks(params, result.startDate, result.endDate+1)
            if(result.taskInstanceList.totalCount > 0) {
                if(result.startDate == result.endDate)
                    result.message = getMessage(code:"task.search.text.all.tasks.message",
                                                                    args:[ formatted(result.startDate) ])
                else
                    result.message = getMessage(code:"task.search.text.all.tasks.between.message",
                                                                    args:[ formatted(result.startDate), formatted(result.endDate) ])
            }
            else {
                if(result.startDate == result.endDate)
                    result.message = getMessage(code:"task.search.text.all.tasks.none.found",
                                                                    args:[ formatted(result.startDate) ])
                else
                    result.message = getMessage(code:"task.search.text.all.tasks.between.none.found",
                                                                    args:[ formatted(result.startDate), formatted(result.endDate) ])
            }

        }

        def personsTasks = {
            result.taskInstanceList = getPersonsTasks(params, result.person, result.startDate, result.endDate+1)
            if(result.taskInstanceList.totalCount > 0) {
                if(result.startDate == result.endDate)
                    result.message = getMessage(code:"task.search.text.persons.tasks.message",
                                                                    args:[ result.person, formatted(result.startDate) ])
                else
                    result.message = getMessage(code:"task.search.text.persons.tasks.between.message",
                                                                    args:[ result.person, formatted(result.startDate), formatted(result.endDate) ])
            }
            else {
                if(result.startDate == result.endDate)
                    result.message = getMessage(code:"task.search.text.persons.tasks.none.found",
                                                                    args:[ result.person, formatted(result.startDate) ])
                else
                    result.message = getMessage(code:"task.search.text.persons.tasks.between.none.found",
                                                                    args:[ result.person, formatted(result.startDate), formatted(result.endDate) ])
            }

        }

        def personsImmediateCallouts = {
            result.taskInstanceList = getPersonsImmediateCallouts(params, result.person, result.startDate, result.endDate+1)
            if(result.taskInstanceList.totalCount > 0) {
                if(result.startDate == result.endDate)
                    result.message = getMessage(code:"task.search.text.persons.immediate.callouts.message",
                                                                    args:[ result.person, formatted(result.startDate) ])
                else
                    result.message = getMessage(code:"task.search.text.persons.immediate.callouts.between.message",
                                                                    args:[ result.person, formatted(result.startDate), formatted(result.endDate) ])
            }
            else {
                if(result.startDate == result.endDate)
                    result.message = getMessage(code:"task.search.text.persons.immediate.callouts.none.found",
                                                                    args:[ result.person, formatted(result.startDate) ])
                else
                    result.message = getMessage(code:"task.search.text.persons.immediate.callouts.between.none.found",
                                                                    args:[ result.person, formatted(result.startDate), formatted(result.endDate) ])
            }

        }

        switch (result.quickSearch) {
            case "myTodays":
                result.quickSearch = "personsTasks"
                result.startDate = startOfToday
                result.endDate = startOfToday
                personsTasks()
                break
            case "myYesterdays":
                result.quickSearch = "personsTasks"
                result.startDate = startOfYesterday
                result.endDate = startOfYesterday
                personsTasks()
                break
            case "myTomorrows":
                result.quickSearch = "personsTasks"
                result.startDate = startOfTomorrow
                result.endDate = startOfTomorrow
                personsTasks()
                break
            case "myPastWeek":
                result.quickSearch = "personsTasks"
                result.startDate = oneWeekAgo
                result.endDate = startOfToday
                personsTasks()
                break
            case "personsTasks":
                personsTasks()
                break
            case "personsImmediateCallouts":
                personsImmediateCallouts()
                break
            case "todays":
                result.quickSearch = "allTasks"
                result.startDate = startOfToday
                result.endDate = startOfToday
                allTasks()
                break
            case "yesterdays":
                result.quickSearch = "allTasks"
                result.startDate = startOfYesterday
                result.endDate = startOfToday
                allTasks()
                break
            case "tomorrows":
                result.quickSearch = "allTasks"
                result.startDate = startOfTomorrow
                result.endDate = startOfTomorrow+1
                allTasks()
                break
            case "pastWeek":
                result.quickSearch = "allTasks"
                result.startDate = oneWeekAgo
                result.endDate = startOfTomorrow
                allTasks()
                break
            case "allTasks":
                allTasks()
                break
            case "budgetUnplanned":
                result.taskInstanceList = getBudgetTasks(params, TaskBudgetStatus.read(1), result.startDate, result.endDate+1)
                if(result.taskInstanceList.totalCount > 0) {
                    if(result.startDate == result.endDate)
                        result.message = getMessage(code:"task.search.text.budget.unplanned.message",
                                                                        args:[ formatted(result.startDate) ])
                    else
                        result.message = getMessage(code:"task.search.text.budget.unplanned.between.message",
                                                                        args:[ formatted(result.startDate), formatted(result.endDate) ])
                }
                else {
                    if(result.startDate == result.endDate)
                        result.message = getMessage(code:"task.search.text.budget.unplanned.none.found",
                                                                        args:[ formatted(result.startDate) ])
                    else
                        result.message = getMessage(code:"task.search.text.budget.unplanned.between.none.found",
                                                                        args:[ formatted(result.startDate), formatted(result.endDate) ])
                }
                break
            case "budgetPlanned":
                result.taskInstanceList = getBudgetTasks(params, TaskBudgetStatus.read(2), result.startDate, result.endDate+1)
                if(result.taskInstanceList.totalCount > 0) {
                    if(result.startDate == result.endDate)
                        result.message = getMessage(code:"task.search.text.budget.planned.message",
                                                                        args:[ formatted(result.startDate) ])
                    else
                        result.message = getMessage(code:"task.search.text.budget.planned.between.message",
                                                                        args:[ formatted(result.startDate), formatted(result.endDate) ])
                }
                else {
                    if(result.startDate == result.endDate)
                        result.message = getMessage(code:"task.search.text.budget.planned.none.found",
                                                                        args:[ formatted(result.startDate) ])
                    else
                        result.message = getMessage(code:"task.search.text.budget.planned.between.none.found",
                                                                        args:[ formatted(result.startDate), formatted(result.endDate) ])
                }
                break
            default:
                //case "plannersRange":
                result.quickSearch = "allTasks"
                result.startDate = oneWeekAgo
                result.endDate = startOfToday+15
                allTasks()
                break
        } // switch.

        // Success.
        return result

    } // getQuickSearch

    /**
    * Get all tasks that are not in the trash bin, by default today's tasks.
    * @param params The request params.
    * @param startDate The start date to get tasks for, defaults to the start of today and is inclusive (greater than or equal to).
    * @param endDate The end date to get tasks for, defaults to the start of tomorrow and is exclusive (less than).
    */
    def getTasks(params, startDate=null, endDate=null) {
        def paginateParams = [:]
        paginateParams.max = Math.min(params?.max?.toInteger() ?: 10, paramsMax)
        paginateParams.offset = params?.offset?.toInteger() ?: 0

        def orderBy = ''
        if(params.sort?.contains('.')) // protect against filterpane bug.
            params.sort = null
        if(params.sort && params.order) {
            def sort = "task." + params.sort
            def order = (params.order == "asc") ? "asc" : "desc"
            orderBy = " order by " + sort + ' ' + order
        }
        else
            orderBy = " order by task.taskStatus, task.taskPriority, task.targetStartDate"

        def namedParams = [:]
        namedParams.startDate = startDate ?: dateUtilService.today
        namedParams.endDate = endDate ?: dateUtilService.tomorrow

        def baseQuery = "from Task as task \
                                        where (task.trash = false \
                                                    and task.targetStartDate < :endDate \
                                                    and task.targetCompletionDate >= :startDate \
                                        )"

        def searchQuery = "select distinct task " + baseQuery + orderBy
        def list = Task.executeQuery(searchQuery, namedParams, paginateParams)

        def countQuery = "select count(distinct task) as taskCount " + baseQuery
        def totalCount = Task.executeQuery(countQuery, namedParams)[0].toInteger()

        def taskInstanceList = new PagedResultList(list, totalCount)
        return taskInstanceList
    } // getPTasks()

    /**
    * Get a person's tasks, by default current user and today's tasks.
    * "My tasks and approved tasks that I am assigned to"
    * @param params The request params.
    * @param person The person to get tasks for, defaults to current user.
    * @param startDate The start date to get tasks for, defaults to the start of today and is inclusive (greater than or equal to).
    * @param endDate The end date to get tasks for, defaults to the start of tomorrow and is exclusive (less than).
    */
    def getPersonsTasks(params, person=null, startDate=null, endDate=null) {

        def max = Math.min(params?.max?.toInteger() ?: 10, paramsMax)
        def offset = params?.offset?.toInteger() ?: 0

        def orderBy = ''
        if(params.sort?.contains('.')) // protect against filterpane bug.
            params.sort = null
        if(params.sort && params.order) {
            def sort = "task." + params.sort
            def order = (params.order == "asc") ? "asc" : "desc"
            orderBy = "by " + sort + ' ' + order
        }
        else
            orderBy = "by task.taskStatus, task.taskPriority, task.targetStartDate"

        def q = new HqlBuilder().query {

            select 'count(distinct task) as taskCount'
            from 'Task as task',
                    'left join task.assignedPersons as assignedPersonOfTask',
                    'left join assignedPersonOfTask.person as assignedPerson',
                    'left join task.assignedGroups as assignedGroupOfTask',
                    'left join assignedGroupOfTask.personGroup as personGroup',
                    'left join personGroup.persons as assignedPersonViaGroup',
                    'left join task.taskModifications as taskModification',
                    'left join taskModification.person as createdBy',
                    'left join taskModification.taskModificationType as taskModificationType'
            where 'task.trash = false'
                    and 'task.targetStartDate < :endDate'
                    and 'task.targetCompletionDate >= :startDate'
                    if(!params.includeCompleted) {
                        and 'task.taskStatus.id != 3' // Complete.
                    }
                    and {
                        where '(taskModificationType.id = 1 and createdBy = :person and task.leadPerson = :person)' // Created.
                        or '(task.approved = true and (task.leadPerson = :person or assignedPerson = :person or assignedPersonViaGroup = :person))'
                    }
        }

        q.namedParams.person = person ?: authService.currentUser
        q.namedParams.startDate = startDate ?: dateUtilService.today
        q.namedParams.endDate = endDate ?: dateUtilService.tomorrow

        def totalCount = Task.executeQuery(q.query, q.namedParams)[0].toInteger()

        q.select = "distinct task"
        q.order = orderBy
        def list = Task.executeQuery(q.query, q.namedParams, q.paginateParams)

        def taskInstanceList = new PagedResultList(list, totalCount)
        return taskInstanceList
    } // getPersonsTasks()

    /**
    * Get a person's immediateCallout tasks, by default all users and today's tasks.
    * @param params The request params.
    * @param person The person to get tasks for, defaults to null and therefore all immediateCallouts.
    * @param startDate The start date to get tasks for, defaults to the start of today and is inclusive (greater than or equal to).
    * @param endDate The end date to get tasks for, defaults to the start of tomorrow and is exclusive (less than).
    */
    def getPersonsImmediateCallouts(params, person=null, startDate=null, endDate=null) {

        def max = Math.min(params?.max?.toInteger() ?: 10, paramsMax)
        def offset = params?.offset?.toInteger() ?: 0

        def orderBy = ''
        if(params.sort?.contains('.')) // protect against filterpane bug.
            params.sort = null
        if(params.sort && params.order) {
            def sort = "task." + params.sort
            def order = (params.order == "asc") ? "asc" : "desc"
            orderBy = "by " + sort + ' ' + order
        }
        else
            orderBy = "by task.taskStatus, task.taskPriority, task.targetStartDate"

        def q = new HqlBuilder().query {

            select 'count(distinct task) as taskCount'
            from 'Task as task',
                    'left join task.taskModifications as taskModification',
                    'left join taskModification.person as createdBy',
                    'left join taskModification.taskModificationType as taskModificationType'
            where 'task.taskType.id = 1' // Immediate Callout.
                    and 'task.targetStartDate < :endDate'
                    and 'task.targetCompletionDate >= :startDate'
                    if(!params.includeCompleted) {
                        and 'task.taskStatus.id != 3' // Complete.
                    }
                    if(person) {
                        namedParams.person = person
                        and '( (taskModificationType.id = 1 and createdBy = :person) or task.leadPerson = :person)' // Created or Lead Person.
                    }
                    and 'task.trash = false'
        }

        q.namedParams.startDate = startDate ?: dateUtilService.today
        q.namedParams.endDate = endDate ?: dateUtilService.tomorrow

        def totalCount = Task.executeQuery(q.query, q.namedParams)[0].toInteger()

        q.select = "distinct task"
        q.order = orderBy
        def list = Task.executeQuery(q.query, q.namedParams, q.paginateParams)

        def taskInstanceList = new PagedResultList(list, totalCount)
        return taskInstanceList
    } // getPersonsImmediateCallouts()

    /**
    * Get tasks by budget status, by default planned in the last week.
    * @param params The request params.
    * @param budgetStatus Defaults to planned.
    * @param startDate The start date to get tasks for, defaults to the start of today and is inclusive (greater than or equal to).
    * @param endDate The end date to get tasks for, defaults to the start of tomorrow and is exclusive (less than).
    */
    def getBudgetTasks(params, taskBudgetStatus=null, startDate=null, endDate=null) {
        def paginateParams = [:]
        paginateParams.max = Math.min(params?.max?.toInteger() ?: 10, paramsMax)
        paginateParams.offset = params?.offset?.toInteger() ?: 0

        def sort = "task." + (params?.sort ?: "targetStartDate")
        def order = params?.order == "desc" ? "desc" : "asc"
        def orderBy = " order by " + sort + ' ' + order

        def namedParams = [:]
        namedParams.taskBudgetStatus = taskBudgetStatus ?: TaskBudgetStatus.read(2) // Planned.
        namedParams.startDate = startDate ?: dateUtilService.today
        namedParams.endDate = endDate ?: dateUtilService.tomorrow

        def baseQuery = "from Task as task \
                                        where (task.trash = false \
                                                    and task.taskBudgetStatus = :taskBudgetStatus \
                                                    and task.targetStartDate < :endDate \
                                                    and task.targetCompletionDate >= :startDate \
                                        )"

        def searchQuery = "select distinct task " + baseQuery + orderBy
        def list = Task.executeQuery(searchQuery, namedParams, paginateParams)

        def countQuery = "select count(distinct task) as taskCount " + baseQuery
        def totalCount = Task.executeQuery(countQuery, namedParams)[0].toInteger()

        def taskInstanceList = new PagedResultList(list, totalCount)
        return taskInstanceList
    } // getBudgetTasks()

    /**
    * Get work done by person and date.
    * A person ID and date may be specified in params otherwise the current user and today are used.
    * @param params The request params.
    * @returns A map containing entries, totalEntries, startOfDay, person, totalHours, totalMinutes.
    */
    def getWorkDone(params, locale) {
        def result = [:]
        result.person = params.person?.id ? Person.get(params.person.id.toInteger()) : authService.currentUser

        if(params.date_year && params.date_month && params.date_day)
            result.startOfDay = dateUtilService.makeDate(params.date_year, params.date_month, params.date_day)
        else
            result.startOfDay = dateUtilService.today

        result.startOfNextDay = result.startOfDay + 1

        def formattedStartOfDay = g.formatDate(format: "EEE, dd-MMM-yyyy", date: result.startOfDay)

        def getMessage = { Map m ->
            messageSource.getMessage(m.code, m.args == null ? null : m.args.toArray(), locale)
        }

        result.entries = Entry.createCriteria().list() {
            eq("enteredBy", result.person)
            ge("dateDone", result.startOfDay)
            lt("dateDone", result.startOfNextDay)
            entryType {
                eq("id", 3L)
            }
        } // createCriteria

        result.totalEntries = result.entries.size()

        if(result.totalEntries > 0)
            result.message = getMessage(code:"task.search.text.work.done.message",
                                                                args:[result.person, formattedStartOfDay])
        else
            result.message = getMessage(code:"task.search.text.work.done.none.found",
                                                                args:[result.person, formattedStartOfDay])

        result.totalHours = 0
        result.totalMinutes = 0
        result.entries.each() {
            result.totalMinutes += (it.durationHour*60) + it.durationMinute
        }
        result.totalHours = (result.totalMinutes / 60).toInteger()
        result.totalMinutes = result.totalMinutes % 60

        return result
    } // getWorkDone()

    /**
    * Get work load by task group and date.
    * Group ID's and date range may be specified in params otherwise no group and today are used.
    * @param params The request params.
    * @returns A map containing the results.
    */
    def getWorkLoad(params, locale) {
        def result = [:]
        def max = 1000

        // TaskStatus..
        result.taskStatusList = []
        if(params.taskStatusList instanceof String)
            result.taskStatusList << TaskStatus.get(params.taskStatusList.toInteger())
        else if(params.taskStatusList)
            result.taskStatusList = TaskStatus.getAll( params.taskStatusList.collect {it.toInteger()} )

        // TaskGroups.
        result.taskGroups = []
        if(params.taskGroups instanceof String)
            result.taskGroups << TaskGroup.get(params.taskGroups.toInteger())
        else if(params.taskGroups)
            result.taskGroups = TaskGroup.getAll( params.taskGroups.collect {it.toInteger()} )

        // Start Date.
        if(params.startDate_year && params.startDate_month && params.startDate_day)
            result.startDate = dateUtilService.makeDate(params.startDate_year, params.startDate_month, params.startDate_day)
        else
            result.startDate = dateUtilService.today

        // End Date.
        if(params.endDate_year && params.endDate_month && params.endDate_day)
            result.endDate = dateUtilService.makeDate(params.endDate_year, params.endDate_month, params.endDate_day)
        else
            result.endDate = result.startDate

        // Auto swap date range.
        if(result.startDate > result.endDate) {
            def tempStartDate = result.startDate
            result.startDate = result.endDate
            result.endDate = tempStartDate
        }

        def formattedStartDate = g.formatDate(format: "EEE, dd-MMM-yyyy", date: result.startDate)
        def formattedEndDate = g.formatDate(format: "EEE, dd-MMM-yyyy", date: result.endDate)

        def getMessage = { Map m ->
            messageSource.getMessage(m.code, m.args == null ? null : m.args.toArray(), locale)
        }

        result.tasks = new PagedResultList([], 0)

        if(result.taskGroups && result.taskStatusList) {

            result.tasks = Task.createCriteria().list(max: max) {
                eq("trash", false)
                lt("targetStartDate", result.endDate+1)
                ge("targetCompletionDate", result.startDate)
                inList("taskStatus", result.taskStatusList)
                inList("taskGroup", result.taskGroups)
                order("taskStatus", "asc")
                order("taskPriority", "asc")
                order("targetStartDate", "asc")
                fetchMode("assignedGroups", FM.EAGER)
                fetchMode("assignedGroups.personGroup", FM.EAGER)
            } // createCriteria

        }

        result.tasks.list.unique()
        result.totalHours = 0
        result.totalMinutes = 0
        result.workLoadGroups = [:]

        // Exit early!
        if(result.tasks.totalCount > result.tasks.size()) {
            result.errorMessage = getMessage(code:"task.search.text.work.load.too.many.results",
                                                                args:[result.tasks.size(), result.tasks.totalCount])
            return result
        }
        else if(result.tasks.size() > 0)
            result.message = getMessage(code:"task.search.text.work.load.message",
                                                                args:[formattedStartDate, formattedEndDate])
        else
            result.message = getMessage(code:"task.search.text.work.load.none.found",
                                                                args:[formattedStartDate, formattedEndDate])

        // Collect all assignedGroups.
        def assignedGroups = []
        for(task in result.tasks) {
            for(assignedGroup in task.assignedGroups) {
                assignedGroups << assignedGroup
            }
        }

        // Calculate work load for each personGroup and minute totals.
        def tempHours = 0
        def tempMinutes = 0
        def personGroup
        for(assignedGroup in assignedGroups) {
            personGroup = assignedGroup.personGroup
            if(!result.workLoadGroups.containsKey(personGroup)) {
                result.workLoadGroups[personGroup] = [hours: 0, minutes: 0]
            }

            tempMinutes = (assignedGroup.estimatedHour*60) + assignedGroup.estimatedMinute
            result.totalMinutes += tempMinutes
            result.workLoadGroups[personGroup].minutes += tempMinutes
        }

        // Resolve totals and sort.
        result.workLoadGroups.each { workLoadGroup ->
            workLoadGroup.value.hours =  (workLoadGroup.value.minutes / 60).toInteger()
            workLoadGroup.value.minutes = workLoadGroup.value.minutes % 60
        }
        result.workLoadGroups = result.workLoadGroups.sort { p1, p2 -> p1.key.name.compareToIgnoreCase(p2.key.name) }
        result.totalHours = (result.totalMinutes / 60).toInteger()
        result.totalMinutes = result.totalMinutes % 60

        // Success.
        return result
    } // getWorkLoad()

} // end class
