// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee 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 Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package models

import (
	"strings"

	"code.vikunja.io/api/pkg/user"
	"code.vikunja.io/web"

	"xorm.io/xorm"
)

// TaskCollection is a struct used to hold filter details and not clutter the Task struct with information not related to actual tasks.
type TaskCollection struct {
	ProjectID     int64 `param:"project" json:"-"`
	ProjectViewID int64 `param:"view" json:"-"`

	// The query parameter to sort by. This is for ex. done, priority, etc.
	SortBy    []string `query:"sort_by" json:"sort_by"`
	SortByArr []string `query:"sort_by[]" json:"-"`
	// The query parameter to order the items by. This can be either asc or desc, with asc being the default.
	OrderBy    []string `query:"order_by" json:"order_by"`
	OrderByArr []string `query:"order_by[]" json:"-"`

	// The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation.
	Filter string `query:"filter" json:"filter"`
	// The time zone which should be used for date match (statements like "now" resolve to different actual times)
	FilterTimezone string `query:"filter_timezone" json:"-"`

	// If set to true, the result will also include null values
	FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`

	isSavedFilter bool

	web.CRUDable `xorm:"-" json:"-"`
	web.Rights   `xorm:"-" json:"-"`
}

func validateTaskField(fieldName string) error {
	switch fieldName {
	case
		taskPropertyID,
		taskPropertyTitle,
		taskPropertyDescription,
		taskPropertyDone,
		taskPropertyDoneAt,
		taskPropertyDueDate,
		taskPropertyCreatedByID,
		taskPropertyProjectID,
		taskPropertyRepeatAfter,
		taskPropertyPriority,
		taskPropertyStartDate,
		taskPropertyEndDate,
		taskPropertyHexColor,
		taskPropertyPercentDone,
		taskPropertyUID,
		taskPropertyCreated,
		taskPropertyUpdated,
		taskPropertyPosition,
		taskPropertyBucketID,
		taskPropertyIndex:
		return nil
	}
	return ErrInvalidTaskField{TaskField: fieldName}
}

func getTaskFilterOptsFromCollection(tf *TaskCollection, projectView *ProjectView) (opts *taskSearchOptions, err error) {
	if len(tf.SortByArr) > 0 {
		tf.SortBy = append(tf.SortBy, tf.SortByArr...)
	}

	if len(tf.OrderByArr) > 0 {
		tf.OrderBy = append(tf.OrderBy, tf.OrderByArr...)
	}

	var sort = make([]*sortParam, 0, len(tf.SortBy))
	for i, s := range tf.SortBy {
		param := &sortParam{
			sortBy:  s,
			orderBy: orderAscending,
		}
		// This checks if tf.OrderBy has an entry with the same index as the current entry from tf.SortBy
		// Taken from https://stackoverflow.com/a/27252199/10924593
		if len(tf.OrderBy) > i {
			param.orderBy = getSortOrderFromString(tf.OrderBy[i])
		}

		if s == taskPropertyPosition && projectView != nil && projectView.ID < 0 {
			continue
		}

		if s == taskPropertyPosition && projectView != nil {
			param.projectViewID = projectView.ID
		}

		// Param validation
		if err := param.validate(); err != nil {
			return nil, err
		}
		sort = append(sort, param)
	}

	opts = &taskSearchOptions{
		sortby:             sort,
		filterIncludeNulls: tf.FilterIncludeNulls,
		filter:             tf.Filter,
		filterTimezone:     tf.FilterTimezone,
	}

	opts.parsedFilters, err = getTaskFiltersFromFilterString(tf.Filter, tf.FilterTimezone)
	return opts, err
}

func getTaskOrTasksInBuckets(s *xorm.Session, a web.Auth, projects []*Project, view *ProjectView, opts *taskSearchOptions) (tasks interface{}, resultCount int, totalItems int64, err error) {
	if view != nil && !strings.Contains(opts.filter, "bucket_id") {
		if view.BucketConfigurationMode != BucketConfigurationModeNone {
			tasksInBuckets, err := GetTasksInBucketsForView(s, view, projects, opts, a)
			return tasksInBuckets, len(tasksInBuckets), int64(len(tasksInBuckets)), err
		}
	}

	return getTasksForProjects(s, projects, a, opts, view)
}

func getRelevantProjectsFromCollection(s *xorm.Session, a web.Auth, tf *TaskCollection) (projects []*Project, err error) {
	if tf.ProjectID == 0 || tf.isSavedFilter {
		projects, _, _, err = getRawProjectsForUser(
			s,
			&projectOptions{
				user: &user.User{ID: a.GetID()},
				page: -1,
			},
		)
		return projects, err
	}

	// Check the project exists and the user has access on it
	project := &Project{ID: tf.ProjectID}
	canRead, _, err := project.CanRead(s, a)
	if err != nil {
		return nil, err
	}
	if !canRead {
		return nil, ErrUserDoesNotHaveAccessToProject{
			ProjectID: tf.ProjectID,
			UserID:    a.GetID(),
		}
	}

	return []*Project{{ID: tf.ProjectID}}, nil
}

// ReadAll gets all tasks for a collection
// @Summary Get tasks in a project
// @Description Returns all tasks for the current project.
// @tags task
// @Accept json
// @Produce json
// @Param id path int true "The project ID."
// @Param view path int true "The project view ID."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search tasks by task text."
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
// @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`."
// @Param filter query string false "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature."
// @Param filter_timezone query string false "The time zone which should be used for date match (statements like "now" resolve to different actual times)"
// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`."
// @Security JWTKeyAuth
// @Success 200 {array} models.Task "The tasks"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id}/views/{view}/tasks [get]
func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {

	// If the project id is < -1 this means we're dealing with a saved filter - in that case we get and populate the filter
	// -1 is the favorites project which works as intended
	if !tf.isSavedFilter && tf.ProjectID < -1 {
		sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(tf.ProjectID))
		if err != nil {
			return nil, 0, 0, err
		}

		// By prepending sort options before the saved ones from the filter, we make sure the supplied sort
		// options via query take precedence over the rest.

		sortby := append(tf.SortBy, tf.SortByArr...)
		sortby = append(sortby, sf.Filters.SortBy...)
		sortby = append(sortby, sf.Filters.SortByArr...)

		orderby := append(tf.OrderBy, tf.OrderByArr...)
		orderby = append(orderby, sf.Filters.OrderBy...)
		orderby = append(orderby, sf.Filters.OrderByArr...)

		sf.Filters.SortBy = sortby
		sf.Filters.SortByArr = nil
		sf.Filters.OrderBy = orderby
		sf.Filters.OrderByArr = nil

		if sf.Filters.FilterTimezone == "" {
			u, err := user.GetUserByID(s, a.GetID())
			if err != nil {
				return nil, 0, 0, err
			}
			sf.Filters.FilterTimezone = u.Timezone
		}

		tc := sf.getTaskCollection()
		tc.ProjectViewID = tf.ProjectViewID
		tc.ProjectID = tf.ProjectID
		tc.isSavedFilter = true

		return tc.ReadAll(s, a, search, page, perPage)
	}

	var view *ProjectView
	if tf.ProjectViewID != 0 {
		view, err = GetProjectViewByIDAndProject(s, tf.ProjectViewID, tf.ProjectID)
		if err != nil {
			return nil, 0, 0, err
		}

		if view.Filter != "" {
			if tf.Filter != "" {
				tf.Filter = "(" + tf.Filter + ") && (" + view.Filter + ")"
			} else {
				tf.Filter = view.Filter
			}
		}
	}

	opts, err := getTaskFilterOptsFromCollection(tf, view)
	if err != nil {
		return nil, 0, 0, err
	}

	opts.search = search
	opts.page = page
	opts.perPage = perPage

	if view != nil {
		var hasOrderByPosition bool
		for _, param := range opts.sortby {
			if param.sortBy == taskPropertyPosition {
				hasOrderByPosition = true
				break
			}
		}
		if !hasOrderByPosition {
			opts.sortby = append(opts.sortby, &sortParam{
				projectViewID: view.ID,
				sortBy:        taskPropertyPosition,
				orderBy:       orderAscending,
			})
		}
	}

	shareAuth, is := a.(*LinkSharing)
	if is {
		project, err := GetProjectSimpleByID(s, shareAuth.ProjectID)
		if err != nil {
			return nil, 0, 0, err
		}
		return getTaskOrTasksInBuckets(s, a, []*Project{project}, view, opts)
	}

	projects, err := getRelevantProjectsFromCollection(s, a, tf)
	if err != nil {
		return nil, 0, 0, err
	}

	return getTaskOrTasksInBuckets(s, a, projects, view, opts)
}
