###
# Copyright (C) 2014-2018 Taiga Agile LLC
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: modules/issues/list.coffee
###

taiga = @.taiga

mixOf = @.taiga.mixOf
trim = @.taiga.trim
toString = @.taiga.toString
joinStr = @.taiga.joinStr
groupBy = @.taiga.groupBy
bindOnce = @.taiga.bindOnce
debounceLeading = @.taiga.debounceLeading
startswith = @.taiga.startswith
bindMethods = @.taiga.bindMethods
debounceLeading = @.taiga.debounceLeading

module = angular.module("taigaIssues")

#############################################################################
## Issues Controller
#############################################################################

class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin)
    @.$inject = [
        "$scope",
        "$rootScope",
        "$tgRepo",
        "$tgConfirm",
        "$tgResources",
        "$tgUrls",
        "$routeParams",
        "$q",
        "$tgLocation",
        "tgAppMetaService",
        "$tgNavUrls",
        "$tgEvents",
        "$tgAnalytics",
        "$translate",
        "tgErrorHandlingService",
        "$tgStorage",
        "tgFilterRemoteStorageService",
        "tgProjectService",
        "tgUserActivityService"
    ]

    filtersHashSuffix: "issues-filters"
    myFiltersHashSuffix: "issues-my-filters"
    excludePrefix: "exclude_"
    filterCategories: [
        "tags",
        "status",
        "type",
        "severity",
        "priority",
        "assigned_to",
        "owner",
        "role",
    ]


    constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appMetaService,
                  @navUrls, @events, @analytics, @translate, @errorHandlingService, @storage, @filterRemoteStorageService, @projectService) ->
        bindMethods(@)

        @scope.sectionName = @translate.instant("PROJECT.SECTION.ISSUES")
        @.voting = false

        return if @.applyStoredFilters(@params.pslug, @.filtersHashSuffix)

        promise = @.loadInitialData()

        # On Success
        promise.then =>
            title = @translate.instant("ISSUES.PAGE_TITLE", {projectName: @scope.project.name})
            description = @translate.instant("ISSUES.PAGE_DESCRIPTION", {
                projectName: @scope.project.name,
                projectDescription: @scope.project.description
            })
            @appMetaService.setAll(title, description)

        # On Error
        promise.then null, @.onInitialDataError.bind(@)

        @scope.$on "issueform:new:success", =>
            @analytics.trackEvent("issue", "create", "create issue on issues list", 1)
            @.loadIssues()

        @scope.$on "assigned-to:changed", =>
            @.generateFilters()
            if @.isFilterDataTypeSelected('assigned_to') ||\
                @.isFilterDataTypeSelected('role') ||\
                @.isOrderedBy('assigned_to') || @.isOrderedBy('modified')
                    @.loadIssues()

        @scope.$on "status:changed", =>
            @.generateFilters()
            if @.isFilterDataTypeSelected('status') ||\
                @.isOrderedBy('status') || @.isOrderedBy('modified')
                    @.loadIssues()

    isOrderedBy: (fieldName) ->
        pattern = new RegExp("-*"+fieldName)
        return pattern.test(@location.search().order_by)

    changeQ: (q) ->
        @.unselectFilter("page")
        @.replaceFilter("q", q)
        @.loadIssues()
        @.generateFilters()

    removeFilter: (filter) ->
        @.unselectFilter("page")
        @.unselectFilter(filter.dataType, filter.id, false, filter.mode)
        @.loadIssues()
        @.generateFilters()

    addFilter: (newFilter) ->
        @.unselectFilter("page")
        @.selectFilter(newFilter.category.dataType, newFilter.filter.id, false, newFilter.mode)
        @.loadIssues()
        @.generateFilters()

    selectCustomFilter: (customFilter) ->
        orderBy = @location.search().order_by

        if orderBy
            customFilter.filter.order_by = orderBy

        @.unselectFilter("page")
        @.replaceAllFilters(customFilter.filter)
        @.loadIssues()
        @.generateFilters()

    removeCustomFilter: (customFilter) ->
        @filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix).then (userFilters) =>
            delete userFilters[customFilter.id]
            @filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.myFiltersHashSuffix).then(@.generateFilters)

    isFilterDataTypeSelected: (filterDataType) ->
        for filter in @.selectedFilters
            if (filter['dataType'] == filterDataType)
                return true
        return false

    saveCustomFilter: (name) ->
        filters = {}
        urlfilters = @location.search()

        for key in @.filterCategories
            excludeKey = @.excludePrefix.concat(key)
            filters[key] = urlfilters[key]
            filters[excludeKey] = urlfilters[excludeKey]

        @filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix).then (userFilters) =>
            userFilters[name] = filters

            @filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.myFiltersHashSuffix).then(@.generateFilters)

    generateFilters: ->
        @.storeFilters(@params.pslug, @location.search(), @.filtersHashSuffix)
        urlfilters = @location.search()

        loadFilters = {}
        loadFilters.project = @scope.projectId
        loadFilters.q = urlfilters.q

        for key in @.filterCategories
            excludeKey = @.excludePrefix.concat(key)
            loadFilters[key] = urlfilters[key]
            loadFilters[excludeKey] = urlfilters[excludeKey]

        return @q.all([
            @rs.issues.filtersData(loadFilters),
            @filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix)
        ]).then (result) =>
            data = result[0]
            customFiltersRaw = result[1]
            dataCollection = {}

            dataCollection.status = _.map data.statuses, (it) ->
                it.id = it.id.toString()

                return it
            dataCollection.type = _.map data.types, (it) ->
                it.id = it.id.toString()

                return it
            dataCollection.severity = _.map data.severities, (it) ->
                it.id = it.id.toString()

                return it
            dataCollection.priority = _.map data.priorities, (it) ->
                it.id = it.id.toString()

                return it
            dataCollection.tags = _.map data.tags, (it) ->
                it.id = it.name

                return it

            tagsWithAtLeastOneElement = _.filter dataCollection.tags, (tag) ->
                return tag.count > 0

            dataCollection.assigned_to = _.map data.assigned_to, (it) ->
                if it.id
                    it.id = it.id.toString()
                else
                    it.id = "null"

                it.name = it.full_name || "Unassigned"

                return it
            dataCollection.owner = _.map data.owners, (it) ->
                it.id = it.id.toString()
                it.name = it.full_name

                return it
            dataCollection.role = _.map data.roles, (it) ->
                if it.id
                    it.id = it.id.toString()
                else
                    it.id = "null"

                it.name = it.name || "Unassigned"

                return it

            @.selectedFilters = []

            for key in @.filterCategories
                excludeKey = @.excludePrefix.concat(key)
                if loadFilters[key]
                    selected = @.formatSelectedFilters(key, dataCollection[key], loadFilters[key])
                    @.selectedFilters = @.selectedFilters.concat(selected)
                if loadFilters[excludeKey]
                    selected = @.formatSelectedFilters(key, dataCollection[key], loadFilters[excludeKey], "exclude")
                    @.selectedFilters = @.selectedFilters.concat(selected)

            @.filterQ = loadFilters.q

            @.filters = [
                {
                    title: @translate.instant("COMMON.FILTERS.CATEGORIES.TYPE"),
                    dataType: "type",
                    content: dataCollection.type
                },
                {
                    title: @translate.instant("COMMON.FILTERS.CATEGORIES.SEVERITY"),
                    dataType: "severity",
                    content: dataCollection.severity
                },
                {
                    title: @translate.instant("COMMON.FILTERS.CATEGORIES.PRIORITIES"),
                    dataType: "priority",
                    content: dataCollection.priority
                },
                {
                    title: @translate.instant("COMMON.FILTERS.CATEGORIES.STATUS"),
                    dataType: "status",
                    content: dataCollection.status
                },
                {
                    title: @translate.instant("COMMON.FILTERS.CATEGORIES.TAGS"),
                    dataType: "tags",
                    content: dataCollection.tags,
                    hideEmpty: true,
                    totalTaggedElements: tagsWithAtLeastOneElement.length
                },
                {
                    title: @translate.instant("COMMON.FILTERS.CATEGORIES.ASSIGNED_TO"),
                    dataType: "assigned_to",
                    content: dataCollection.assigned_to
                },
                {
                    title: @translate.instant("COMMON.FILTERS.CATEGORIES.ROLE"),
                    dataType: "role",
                    content: dataCollection.role
                },
                {
                    title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"),
                    dataType: "owner",
                    content: dataCollection.owner
                }
            ]

            @.customFilters = []
            _.forOwn customFiltersRaw, (value, key) =>
                @.customFilters.push({id: key, name: key, filter: value})

    initializeSubscription: ->
        routingKey = "changes.project.#{@scope.projectId}.issues"
        @events.subscribe @scope, routingKey, debounceLeading(500, (message) =>
            @.loadIssues())

    loadProject: ->
        project = @projectService.project.toJS()

        if not project.is_issues_activated
            @errorHandlingService.permissionDenied()

        @scope.projectId = project.id
        @scope.project = project
        @scope.$emit('project:loaded', project)

        @scope.issueStatusById = groupBy(project.issue_statuses, (x) -> x.id)
        @scope.issueStatusList = _.sortBy(project.issue_statuses, "order")
        @scope.severityById = groupBy(project.severities, (x) -> x.id)
        @scope.severityList = _.sortBy(project.severities, "order")
        @scope.priorityById = groupBy(project.priorities, (x) -> x.id)
        @scope.priorityList = _.sortBy(project.priorities, "order")
        @scope.issueTypes = _.sortBy(project.issue_types, "order")
        @scope.issueTypeById = groupBy(project.issue_types, (x) -> x.id)

        return project

    # We need to guarantee that the last petition done here is the finally used
    # When searching by text loadIssues can be called fastly with different parameters and
    # can be resolved in a different order than generated
    # We count the requests made and only if the callback is for the last one data is updated
    loadIssuesRequests: 0
    loadIssues: =>
        params = @location.search()

        promise = @rs.issues.list(@scope.projectId, params)
        @.loadIssuesRequests += 1
        promise.index = @.loadIssuesRequests
        promise.then (data) =>
            if promise.index == @.loadIssuesRequests
                @scope.issues = data.models
                @scope.page = data.current
                @scope.count = data.count
                @scope.paginatedBy = data.paginatedBy

            return data

        return promise

    loadInitialData: ->
        project = @.loadProject()

        @.fillUsersAndRoles(project.members, project.roles)
        @.initializeSubscription()
        @.generateFilters()

        return @.loadIssues()

    # Functions used from templates
    addNewIssue: ->
        project = @projectService.project.toJS()
        @rootscope.$broadcast("genericform:new", {
            'objType': 'issue',
            'project': project
        })

    addIssuesInBulk: ->
        @rootscope.$broadcast("issueform:bulk", @scope.projectId)

    upVoteIssue: (issueId) ->
        @.voting = issueId
        onSuccess = =>
            @.loadIssues()
            @.voting = null
        onError = =>
            @confirm.notify("error")
            @.voting = null

        return @rs.issues.upvote(issueId).then(onSuccess, onError)

    downVoteIssue: (issueId) ->
        @.voting = issueId
        onSuccess = =>
            @.loadIssues()
            @.voting = null
        onError = =>
            @confirm.notify("error")
            @.voting = null

        return @rs.issues.downvote(issueId).then(onSuccess, onError)

    getOrderBy: ->
        if _.isString(@location.search().order_by)
            return @location.search().order_by
        else
            return "created_date"

module.controller("IssuesController", IssuesController)

#############################################################################
## Issues Directive
#############################################################################

IssuesDirective = ($log, $location, $template, $compile) ->
    ## Issues Pagination
    template = $template.get("issue/issue-paginator.html", true)

    linkPagination = ($scope, $el, $attrs, $ctrl) ->
        # Constants
        afterCurrent = 2
        beforeCurrent = 4
        atBegin = 2
        atEnd = 2

        $pagEl = $el.find(".issues-paginator")

        getNumPages = ->
            numPages = $scope.count / $scope.paginatedBy
            if parseInt(numPages, 10) < numPages
                numPages = parseInt(numPages, 10) + 1
            else
                numPages = parseInt(numPages, 10)

            return numPages

        renderPagination = ->
            numPages = getNumPages()

            if numPages <= 1
                $pagEl.hide()
                return
            $pagEl.show()

            pages = []
            options = {}
            options.pages = pages
            options.showPrevious = ($scope.page > 1)
            options.showNext = not ($scope.page == numPages)

            cpage = $scope.page

            for i in [1..numPages]
                if i == (cpage + afterCurrent) and numPages > (cpage + afterCurrent + atEnd)
                    pages.push({classes: "dots", type: "dots"})
                else if i == (cpage - beforeCurrent) and cpage > (atBegin + beforeCurrent)
                    pages.push({classes: "dots", type: "dots"})
                else if i > (cpage + afterCurrent) and i <= (numPages - atEnd)
                else if i < (cpage - beforeCurrent) and i > atBegin
                else if i == cpage
                    pages.push({classes: "active", num: i, type: "page-active"})
                else
                    pages.push({classes: "page", num: i, type: "page"})


            html = template(options)
            html = $compile(html)($scope)

            $pagEl.html(html)

        $scope.$watch "issues", (value) ->
            # Do nothing if value is not logical true
            return if not value

            renderPagination()

        $el.on "click", ".issues-paginator a.next", (event) ->
            event.preventDefault()

            $scope.$apply ->
                $ctrl.selectFilter("page", $scope.page + 1)
                $ctrl.loadIssues()

        $el.on "click", ".issues-paginator a.previous", (event) ->
            event.preventDefault()
            $scope.$apply ->
                $ctrl.selectFilter("page", $scope.page - 1)
                $ctrl.loadIssues()

        $el.on "click", ".issues-paginator li.page > a", (event) ->
            event.preventDefault()
            target = angular.element(event.currentTarget)
            pagenum = target.data("pagenum")

            $scope.$apply ->
                $ctrl.selectFilter("page", pagenum)
                $ctrl.loadIssues()

    ## Issues Filters
    linkOrdering = ($scope, $el, $attrs, $ctrl) ->
        # Draw the arrow the first time

        currentOrder = $ctrl.getOrderBy()

        if currentOrder
            icon = if startswith(currentOrder, "-") then "icon-arrow-up" else "icon-arrow-down"
            colHeadElement = $el.find(".row.title > div[data-fieldname='#{trim(currentOrder, "-")}']")

            svg = $("<tg-svg>").attr("svg-icon", icon)

            colHeadElement.append(svg)
            $compile(colHeadElement.contents())($scope)

        $el.on "click", ".row.title > div", (event) ->
            target = angular.element(event.currentTarget)

            currentOrder = $ctrl.getOrderBy()
            newOrder = target.data("fieldname")

            if newOrder == 'total_voters' and currentOrder != "-total_voters"
                currentOrder = "total_voters"
            finalOrder = if currentOrder == newOrder then "-#{newOrder}" else newOrder

            $scope.$apply ->
                $ctrl.replaceFilter("order_by", finalOrder)

                $ctrl.storeFilters($ctrl.params.pslug, $location.search(), $ctrl.filtersHashSuffix)
                $ctrl.loadIssues().then ->
                    # Update the arrow
                    $el.find(".row.title > div > tg-svg").remove()
                    icon = if startswith(finalOrder, "-") then "icon-arrow-up" else "icon-arrow-down"

                    svg = $("<tg-svg>")
                        .attr("svg-icon", icon)

                    target.append(svg)
                    $compile(target.contents())($scope)

    ## Issues Link
    link = ($scope, $el, $attrs) ->
        $ctrl = $el.controller()
        linkOrdering($scope, $el, $attrs, $ctrl)
        linkPagination($scope, $el, $attrs, $ctrl)

        $scope.$on "$destroy", ->
            $el.off()

    return {link:link}

module.directive("tgIssues", ["$log", "$tgLocation", "$tgTemplate", "$compile", IssuesDirective])


#############################################################################
## Issue status Directive (popover for change status)
#############################################################################

IssueStatusInlineEditionDirective = ($repo, $template, $rootscope) ->
    ###
    Print the status of an Issue and a popover to change it.
    - tg-issue-status-inline-edition: The issue

    Example:

      div.status(tg-issue-status-inline-edition="issue")
        a.issue-status(href="")

    NOTE: This directive need 'issueStatusById' and 'project'.
    ###
    selectionTemplate = $template.get("issue/issue-status-inline-edition-selection.html", true)

    updateIssueStatus = ($el, issue, issueStatusById) ->
        issueStatusDomParent = $el.find(".issue-status")
        issueStatusDom = $el.find(".issue-status .issue-status-bind")

        status = issueStatusById[issue.status]

        if status
            issueStatusDom.text(status.name)
            issueStatusDom.prop("title", status.name)
            issueStatusDomParent.css('color', status.color)

    link = ($scope, $el, $attrs) ->
        $ctrl = $el.controller()
        issue = $scope.$eval($attrs.tgIssueStatusInlineEdition)

        $el.on "click", ".issue-status", (event) ->
            event.preventDefault()
            event.stopPropagation()
            $el.find(".pop-status").popover().open()

        $el.on "click", ".status", (event) ->
            event.preventDefault()
            event.stopPropagation()
            target = angular.element(event.currentTarget)

            issue.status = target.data("status-id")
            $el.find(".pop-status").popover().close()
            updateIssueStatus($el, issue, $scope.issueStatusById)

            $scope.$apply () ->
                $repo.save(issue).then (response) ->
                    $rootscope.$broadcast("status:changed", response)

        taiga.bindOnce $scope, "project", (project) ->
            $el.append(selectionTemplate({ 'statuses':  project.issue_statuses }))
            updateIssueStatus($el, issue, $scope.issueStatusById)

            # If the user has not enough permissions the click events are unbinded
            if project.my_permissions.indexOf("modify_issue") == -1
                $el.unbind("click")
                $el.find("a").addClass("not-clickable")

        $scope.$watch $attrs.tgIssueStatusInlineEdition, (val) =>
            updateIssueStatus($el, val, $scope.issueStatusById)

        $scope.$on "$destroy", ->
            $el.off()

    return {link: link}

module.directive("tgIssueStatusInlineEdition", ["$tgRepo", "$tgTemplate", "$rootScope",
                                                IssueStatusInlineEditionDirective])


#############################################################################
## Issue assigned to Directive
#############################################################################

IssueAssignedToInlineEditionDirective = ($repo, $rootscope, $translate, avatarService) ->
    template = _.template("""
    <img style="background-color: <%- bg %>" src="<%- imgurl %>" alt="<%- name %>"/>
    <figcaption><%- name %></figcaption>
    """)

    link = ($scope, $el, $attrs) ->
        updateIssue = (issue) ->
            ctx = {
                name: $translate.instant("COMMON.ASSIGNED_TO.NOT_ASSIGNED"),
                imgurl: "/#{window._version}/images/unnamed.png"
            }

            member = $scope.usersById[issue.assigned_to]

            avatar = avatarService.getAvatar(member)
            ctx.imgurl = avatar.url
            ctx.bg = null

            if member
                ctx.name = member.full_name_display
                ctx.bg = avatar.bg

            $el.find(".avatar").html(template(ctx))
            $el.find(".issue-assignedto").attr('title', ctx.name)

        $ctrl = $el.controller()
        issue = $scope.$eval($attrs.tgIssueAssignedToInlineEdition)
        updateIssue(issue)

        $el.on "click", ".issue-assignedto", (event) ->
            $rootscope.$broadcast("assigned-to:add", issue)

        taiga.bindOnce $scope, "project", (project) ->
            # If the user has not enough permissions the click events are unbinded
            if project.my_permissions.indexOf("modify_issue") == -1
                $el.unbind("click")
                $el.find("a").addClass("not-clickable")

        $scope.$on "assigned-to:added", (ctx, userId, updatedIssue) ->
            if updatedIssue.id == issue.id
                updatedIssue.assigned_to = userId
                $repo.save(issue).then ->
                    updateIssue(updatedIssue)
                    $rootscope.$broadcast("assigned-to:changed", updatedIssue)

        $scope.$watch $attrs.tgIssueAssignedToInlineEdition, (val) ->
            updateIssue(val)

        $scope.$on "$destroy", ->
            $el.off()

    return {link: link}

module.directive("tgIssueAssignedToInlineEdition", ["$tgRepo", "$rootScope", "$translate", "tgAvatarService",
                                                    IssueAssignedToInlineEditionDirective])
