###
# 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/kanban/main.coffee
###

taiga = @.taiga

mixOf = @.taiga.mixOf
toggleText = @.taiga.toggleText
scopeDefer = @.taiga.scopeDefer
bindOnce = @.taiga.bindOnce
groupBy = @.taiga.groupBy
timeout = @.taiga.timeout
bindMethods = @.taiga.bindMethods
debounceLeading = @.taiga.debounceLeading

module = angular.module("taigaKanban")

#############################################################################
## Kanban Controller
#############################################################################

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

    storeCustomFiltersName: 'kanban-custom-filters'
    storeFiltersName: 'kanban-filters'

    constructor: (@scope, @rootscope, @repo, @confirm, @rs, @rs2, @params, @q, @location,
                  @appMetaService, @navUrls, @events, @analytics, @translate, @errorHandlingService,
                  @model, @kanbanUserstoriesService, @storage, @filterRemoteStorageService, @projectService) ->
        bindMethods(@)
        @kanbanUserstoriesService.reset()
        @.openFilter = false
        @.selectedUss = {}

        return if @.applyStoredFilters(@params.pslug, "kanban-filters")

        @scope.sectionName = @translate.instant("KANBAN.SECTION_NAME")
        @.initializeEventHandlers()

        taiga.defineImmutableProperty @.scope, "usByStatus", () =>
            return @kanbanUserstoriesService.usByStatus

    cleanSelectedUss: () ->
        for key of @.selectedUss
            @.selectedUss[key] = false

    toggleSelectedUs: (usId) ->
        @.selectedUss[usId] = !@.selectedUss[usId]

    firstLoad: () ->
        promise = @.loadInitialData()

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

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

    setZoom: (zoomLevel, zoom) ->
        if @.zoomLevel == zoomLevel
            return null

        @.isFirstLoad = !@.zoomLevel

        previousZoomLevel = @.zoomLevel

        @.zoomLevel = zoomLevel
        @.zoom = zoom

        if @.isFirstLoad
            @.firstLoad().then () =>
                @.isFirstLoad = false
                @kanbanUserstoriesService.resetFolds()

        else if @.zoomLevel > 1 && previousZoomLevel <= 1
            @.zoomLoading = true

            @.loadUserstories().then () =>
                @.zoomLoading = false
                @kanbanUserstoriesService.resetFolds()

    filtersReloadContent: () ->
        @.loadUserstories().then () =>
            openArchived = _.difference(@kanbanUserstoriesService.archivedStatus,
                                        @kanbanUserstoriesService.statusHide)
            if openArchived.length
                for statusId in openArchived
                    @.loadUserStoriesForStatus({}, statusId)

    initializeEventHandlers: ->
        @scope.$on "usform:new:success", (event, us) =>
            @.refreshTagsColors().then () =>
                @kanbanUserstoriesService.add(us)

            @analytics.trackEvent("userstory", "create", "create userstory on kanban", 1)

        @scope.$on "usform:bulk:success", (event, uss) =>
            @.refreshTagsColors().then () =>
                @kanbanUserstoriesService.add(uss)

            @analytics.trackEvent("userstory", "create", "bulk create userstory on kanban", 1)

        @scope.$on "usform:edit:success", (event, us) =>
            @.refreshTagsColors().then () =>
                @kanbanUserstoriesService.replaceModel(us)

        @scope.$on "kanban:us:deleted", (event, us) =>
            @.filtersReloadContent()

        @scope.$on("assigned-to:added", @.onAssignedToChanged)
        @scope.$on("assigned-user:added", @.onAssignedUsersChanged)
        @scope.$on("assigned-user:deleted", @.onAssignedUsersDeleted)
        @scope.$on("kanban:us:move", @.moveUs)
        @scope.$on("kanban:show-userstories-for-status", @.loadUserStoriesForStatus)
        @scope.$on("kanban:hide-userstories-for-status", @.hideUserStoriesForStatus)

    addNewUs: (type, statusId) ->
        switch type
            when "standard" then  @rootscope.$broadcast("genericform:new",
                {
                    'objType': 'us',
                    'project': @scope.project,
                    'statusId': statusId
                })
            when "bulk" then @rootscope.$broadcast("usform:bulk",
                                                   @scope.projectId, statusId)

    editUs: (id) ->
        us = @kanbanUserstoriesService.getUs(id)
        us = us.set('loading-edit', true)
        @kanbanUserstoriesService.replace(us)

        @rs.userstories.getByRef(us.getIn(['model', 'project']), us.getIn(['model', 'ref']))
        .then (editingUserStory) =>
            @rs2.attachments.list(
                "us", us.get('id'), us.getIn(['model', 'project'])).then (attachments) =>
                    @rootscope.$broadcast("genericform:edit", {
                        'objType': 'us',
                        'obj': editingUserStory,
                        'statusList': @scope.usStatusList,
                        'attachments': attachments.toJS()
                    })

                us = us.set('loading-edit', false)
                @kanbanUserstoriesService.replace(us)

    deleteUs: (id) ->
        us = @kanbanUserstoriesService.getUs(id)
        us = us.set('loading-delete', true)

        @rs.userstories.getByRef(us.getIn(['model', 'project']), us.getIn(['model', 'ref']))
        .then (deletingUserStory) =>
            us = us.set('loading-delete', false)
            title = @translate.instant("US.TITLE_DELETE_ACTION")
            message = deletingUserStory.subject
            @confirm.askOnDelete(title, message).then (askResponse) =>
                promise = @repo.remove(deletingUserStory)
                promise.then =>
                    @scope.$broadcast("kanban:us:deleted")
                    askResponse.finish()
                promise.then null, ->
                    askResponse.finish(false)
                    @confirm.notify("error")

    showPlaceHolder: (statusId) ->
        if @scope.usStatusList[0].id == statusId &&
          !@kanbanUserstoriesService.userstoriesRaw.length
            return true

        return false

    toggleFold: (id) ->
        @kanbanUserstoriesService.toggleFold(id)

    isUsInArchivedHiddenStatus: (usId) ->
        return @kanbanUserstoriesService.isUsInArchivedHiddenStatus(usId)

    changeUsAssignedTo: (id) ->
        us = @kanbanUserstoriesService.getUsModel(id)

        @rootscope.$broadcast("assigned-to:add", us)

    changeUsAssignedUsers: (id) ->
        us = @kanbanUserstoriesService.getUsModel(id)
        @rootscope.$broadcast("assigned-user:add", us)

    onAssignedToChanged: (ctx, userid, usModel) ->
        usModel.assigned_to = userid

        @kanbanUserstoriesService.replaceModel(usModel)

        @repo.save(usModel).then =>
            @.generateFilters()
            if @.isFilterDataTypeSelected('assigned_to') || @.isFilterDataTypeSelected('role')
                @.filtersReloadContent()

    onAssignedUsersChanged: (ctx, userid, usModel) ->
        assignedUsers = _.clone(usModel.assigned_users, false)
        assignedUsers.push(userid)
        assignedUsers = _.uniq(assignedUsers)
        usModel.assigned_users = assignedUsers
        if not usModel.assigned_to
            usModel.assigned_to = userid
        @kanbanUserstoriesService.replaceModel(usModel)

        @repo.save(usModel).then =>
            @.generateFilters()
            if @.isFilterDataTypeSelected('assigned_users') || @.isFilterDataTypeSelected('role')
                @.filtersReloadContent()

    onAssignedUsersDeleted: (ctx, userid, usModel) ->
        assignedUsersIds = _.clone(usModel.assigned_users, false)
        assignedUsersIds = _.pull(assignedUsersIds, userid)
        assignedUsersIds = _.uniq(assignedUsersIds)
        usModel.assigned_users = assignedUsersIds

        # Update as
        if usModel.assigned_to not in assignedUsersIds and assignedUsersIds.length > 0
            usModel.assigned_to = assignedUsersIds[0]
        if assignedUsersIds.length == 0
            usModel.assigned_to = null

        @kanbanUserstoriesService.replaceModel(usModel)

        @repo.save(usModel).then =>
            @.generateFilters()
            if @.isFilterDataTypeSelected('assigned_users') || @.isFilterDataTypeSelected('role')
                @.filtersReloadContent()

    refreshTagsColors: ->
        return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) =>
            @scope.project.tags_colors = tags_colors._attrs

    loadUserstories: () ->
        params = {
            status__is_archived: false
        }

        if @.zoomLevel > 1
            params.include_attachments = 1
            params.include_tasks = 1

        params = _.merge params, @location.search()

        promise = @rs.userstories.listAll(@scope.projectId, params).then (userstories) =>
            @kanbanUserstoriesService.init(@scope.project, @scope.usersById)
            @kanbanUserstoriesService.set(userstories)

            # The broadcast must be executed when the DOM has been fully reloaded.
            # We can't assure when this exactly happens so we need a defer
            scopeDefer @scope, =>
                @scope.$broadcast("userstories:loaded", userstories)

            return userstories

        promise.then( => @scope.$broadcast("redraw:wip"))

        return promise

    loadUserStoriesForStatus: (ctx, statusId) ->
        filteredStatus = @location.search().status

        # if there are filters applied the action doesn't end if the statusId is not in the url
        if filteredStatus
            filteredStatus = filteredStatus.split(",").map (it) -> parseInt(it, 10)

            return if filteredStatus.indexOf(statusId) == -1

        params = {
            status: statusId
            include_attachments: true,
            include_tasks: true
        }

        params = _.merge params, @location.search()

        return @rs.userstories.listAll(@scope.projectId, params).then (userstories) =>
            @scope.$broadcast("kanban:shown-userstories-for-status", statusId, userstories)

            return userstories

    hideUserStoriesForStatus: (ctx, statusId) ->
        @scope.$broadcast("kanban:hidden-userstories-for-status", statusId)

    loadKanban: ->
        return @q.all([
            @.refreshTagsColors(),
            @.loadUserstories()
        ])

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

        if not project.is_kanban_activated
            @errorHandlingService.permissionDenied()

        @scope.projectId = project.id
        @scope.project = project
        @scope.projectId = project.id
        @scope.points = _.sortBy(project.points, "order")
        @scope.pointsById = groupBy(project.points, (x) -> x.id)
        @scope.usStatusById = groupBy(project.us_statuses, (x) -> x.id)
        @scope.usStatusList = _.sortBy(project.us_statuses, "order")

        @scope.$emit("project:loaded", project)
        return project

    initializeSubscription: ->
        routingKey1 = "changes.project.#{@scope.projectId}.userstories"
        randomTimeout = taiga.randomInt(700, 1000)
        @events.subscribe @scope, routingKey1, debounceLeading(randomTimeout, (message) =>
            @.loadUserstories())

    loadInitialData: ->
        project = @.loadProject()

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

    # Utils methods

    prepareBulkUpdateData: (uses, field="kanban_order") ->
        return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]})

    moveUs: (ctx, usList, newStatusId, index) ->
        @.cleanSelectedUss()

        usList = _.map usList, (us) =>
            return @kanbanUserstoriesService.getUsModel(us.id)

        data = @kanbanUserstoriesService.move(usList, newStatusId, index)

        promise = @rs.userstories.bulkUpdateKanbanOrder(@scope.projectId, newStatusId, data.bulkOrders)

        promise.then () =>
            # saving
            # drag single or different status
            options = {
                headers: {
                    "set-orders": JSON.stringify(data.setOrders)
                }
            }

            params = {
                include_attachments: true,
                include_tasks: true
            }

            promises = _.map usList, (us) =>
                @repo.save(us, true, params, options, true)

            promise = @q.all(promises)

            promise.then (result) =>
                headers = result[1]

                if headers && headers['taiga-info-order-updated']
                    order = JSON.parse(headers['taiga-info-order-updated'])
                    @kanbanUserstoriesService.assignOrders(order)
                @scope.$broadcast("redraw:wip")

                @.generateFilters()
                if @.isFilterDataTypeSelected('status')
                    @.filtersReloadContent()

                return promise


module.controller("KanbanController", KanbanController)

#############################################################################
## Kanban Directive
#############################################################################

KanbanDirective = ($repo, $rootscope) ->
    link = ($scope, $el, $attrs) ->
        tableBodyDom = $el.find(".kanban-table-body")

        tableBodyDom.on "scroll", (event) ->
            target = angular.element(event.currentTarget)
            tableHeaderDom = $el.find(".kanban-table-header .kanban-table-inner")
            tableHeaderDom.css("left", -1 * target.scrollLeft())

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

    return {link: link}

module.directive("tgKanban", ["$tgRepo", "$rootScope", KanbanDirective])

#############################################################################
## Kanban Archived Status Column Header Control
#############################################################################

KanbanArchivedStatusHeaderDirective = ($rootscope, $translate, kanbanUserstoriesService) ->
    showArchivedText = $translate.instant("KANBAN.ACTION_SHOW_ARCHIVED")
    hideArchivedText = $translate.instant("KANBAN.ACTION_HIDE_ARCHIVED")

    link = ($scope, $el, $attrs) ->
        status = $scope.$eval($attrs.tgKanbanArchivedStatusHeader)
        hidden = true

        kanbanUserstoriesService.addArchivedStatus(status.id)
        kanbanUserstoriesService.hideStatus(status.id)

        $scope.class = "icon-watch"
        $scope.title = showArchivedText

        $el.on "click", (event) ->
            hidden = not hidden

            $scope.$apply ->
                if hidden
                    $scope.class = "icon-watch"
                    $scope.title = showArchivedText
                    $rootscope.$broadcast("kanban:hide-userstories-for-status", status.id)

                    kanbanUserstoriesService.hideStatus(status.id)
                else
                    $scope.class = "icon-unwatch"
                    $scope.title = hideArchivedText
                    $rootscope.$broadcast("kanban:show-userstories-for-status", status.id)

                    kanbanUserstoriesService.showStatus(status.id)

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

    return {link:link}

module.directive("tgKanbanArchivedStatusHeader", [ "$rootScope", "$translate", "tgKanbanUserstories", KanbanArchivedStatusHeaderDirective])


#############################################################################
## Kanban Archived Status Column Intro Directive
#############################################################################

KanbanArchivedStatusIntroDirective = ($translate, kanbanUserstoriesService) ->
    userStories = []

    link = ($scope, $el, $attrs) ->
        hiddenUserStoriexText = $translate.instant("KANBAN.HIDDEN_USER_STORIES")
        status = $scope.$eval($attrs.tgKanbanArchivedStatusIntro)
        $el.text(hiddenUserStoriexText)

        updateIntroText = (hasArchived) ->
            if hasArchived
                $el.text("")
            else
                $el.text(hiddenUserStoriexText)

        $scope.$on "kanban:us:move", (ctx, itemUs, oldStatusId, newStatusId, itemIndex) ->
            hasArchived = !!kanbanUserstoriesService.getStatus(newStatusId).length
            updateIntroText(hasArchived)

        $scope.$on "kanban:shown-userstories-for-status", (ctx, statusId, userStoriesLoaded) ->
            if statusId == status.id
                kanbanUserstoriesService.deleteStatus(statusId)
                kanbanUserstoriesService.add(userStoriesLoaded)

                hasArchived = !!kanbanUserstoriesService.getStatus(statusId).length
                updateIntroText(hasArchived)

        $scope.$on "kanban:hidden-userstories-for-status", (ctx, statusId) ->
            if statusId == status.id
                updateIntroText(false)

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

    return {link:link}

module.directive("tgKanbanArchivedStatusIntro", ["$translate", "tgKanbanUserstories", KanbanArchivedStatusIntroDirective])

#############################################################################
## Kanban Squish Column Directive
#############################################################################

KanbanSquishColumnDirective = (rs, projectService) ->
    link = ($scope, $el, $attrs) ->
        $scope.foldStatus = (status) ->
            $scope.folds[status.id] = !!!$scope.folds[status.id]
            rs.kanban.storeStatusColumnModes($scope.projectId, $scope.folds)
            updateTableWidth()
            return

        updateTableWidth = ->
            columnWidths = _.map $scope.usStatusList, (status) ->
                if $scope.folds[status.id]
                    return 40
                else
                    return 310

            totalWidth = _.reduce columnWidths, (total, width) ->
                return total + width

            $el.find('.kanban-table-inner').css("width", totalWidth)

        unwatch = $scope.$watch 'usByStatus', (usByStatus) ->
            if usByStatus.size
                $scope.folds = rs.kanban.getStatusColumnModes(projectService.project.get('id'))
                updateTableWidth()

                unwatch()

    return {link: link}

module.directive("tgKanbanSquishColumn", ["$tgResources", "tgProjectService", KanbanSquishColumnDirective])

#############################################################################
## Kanban WIP Limit Directive
#############################################################################

KanbanWipLimitDirective = ($timeout) ->
    link = ($scope, $el, $attrs) ->
        status = $scope.$eval($attrs.tgKanbanWipLimit)

        redrawWipLimit = =>
            $el.find(".kanban-wip-limit").remove()
            $timeout =>
                element = $el.find("tg-card")[status.wip_limit]
                if element
                    angular.element(element).before("<div class='kanban-wip-limit'></div>")

        if status and not status.is_archived
            $scope.$on "redraw:wip", redrawWipLimit
            $scope.$on "kanban:us:move", redrawWipLimit
            $scope.$on "usform:new:success", redrawWipLimit
            $scope.$on "usform:bulk:success", redrawWipLimit

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

    return {link: link}

module.directive("tgKanbanWipLimit", ["$timeout", KanbanWipLimitDirective])
