// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package db

import (
	"context"

	"code.gitea.io/gitea/modules/setting"

	"xorm.io/builder"
	"xorm.io/xorm"
)

const (
	// DefaultMaxInSize represents default variables number on IN () in SQL
	DefaultMaxInSize     = 50
	defaultFindSliceSize = 10
)

// Paginator is the base for different ListOptions types
type Paginator interface {
	GetSkipTake() (skip, take int)
	IsListAll() bool
}

// SetSessionPagination sets pagination for a database session
func SetSessionPagination(sess Engine, p Paginator) *xorm.Session {
	skip, take := p.GetSkipTake()

	return sess.Limit(take, skip)
}

// ListOptions options to paginate results
type ListOptions struct {
	PageSize int
	Page     int  // start from 1
	ListAll  bool // if true, then PageSize and Page will not be taken
}

var ListOptionsAll = ListOptions{ListAll: true}

var (
	_ Paginator   = &ListOptions{}
	_ FindOptions = ListOptions{}
)

// GetSkipTake returns the skip and take values
func (opts *ListOptions) GetSkipTake() (skip, take int) {
	opts.SetDefaultValues()
	return (opts.Page - 1) * opts.PageSize, opts.PageSize
}

func (opts ListOptions) GetPage() int {
	return opts.Page
}

func (opts ListOptions) GetPageSize() int {
	return opts.PageSize
}

// IsListAll indicates PageSize and Page will be ignored
func (opts ListOptions) IsListAll() bool {
	return opts.ListAll
}

// SetDefaultValues sets default values
func (opts *ListOptions) SetDefaultValues() {
	if opts.PageSize <= 0 {
		opts.PageSize = setting.API.DefaultPagingNum
	}
	if opts.PageSize > setting.API.MaxResponseItems {
		opts.PageSize = setting.API.MaxResponseItems
	}
	if opts.Page <= 0 {
		opts.Page = 1
	}
}

func (opts ListOptions) ToConds() builder.Cond {
	return builder.NewCond()
}

// AbsoluteListOptions absolute options to paginate results
type AbsoluteListOptions struct {
	skip int
	take int
}

var _ Paginator = &AbsoluteListOptions{}

// NewAbsoluteListOptions creates a list option with applied limits
func NewAbsoluteListOptions(skip, take int) *AbsoluteListOptions {
	if skip < 0 {
		skip = 0
	}
	if take <= 0 {
		take = setting.API.DefaultPagingNum
	}
	if take > setting.API.MaxResponseItems {
		take = setting.API.MaxResponseItems
	}
	return &AbsoluteListOptions{skip, take}
}

// IsListAll will always return false
func (opts *AbsoluteListOptions) IsListAll() bool {
	return false
}

// GetSkipTake returns the skip and take values
func (opts *AbsoluteListOptions) GetSkipTake() (skip, take int) {
	return opts.skip, opts.take
}

// FindOptions represents a find options
type FindOptions interface {
	GetPage() int
	GetPageSize() int
	IsListAll() bool
	ToConds() builder.Cond
}

type JoinFunc func(sess Engine) error

type FindOptionsJoin interface {
	ToJoins() []JoinFunc
}

type FindOptionsOrder interface {
	ToOrders() string
}

// Find represents a common find function which accept an options interface
func Find[T any](ctx context.Context, opts FindOptions) ([]*T, error) {
	sess := GetEngine(ctx)

	if joinOpt, ok := opts.(FindOptionsJoin); ok && len(joinOpt.ToJoins()) > 0 {
		for _, joinFunc := range joinOpt.ToJoins() {
			if err := joinFunc(sess); err != nil {
				return nil, err
			}
		}
	}

	sess = sess.Where(opts.ToConds())
	page, pageSize := opts.GetPage(), opts.GetPageSize()
	if !opts.IsListAll() && pageSize > 0 {
		if page == 0 {
			page = 1
		}
		sess.Limit(pageSize, (page-1)*pageSize)
	}
	if newOpt, ok := opts.(FindOptionsOrder); ok && newOpt.ToOrders() != "" {
		sess.OrderBy(newOpt.ToOrders())
	}

	findPageSize := defaultFindSliceSize
	if pageSize > 0 {
		findPageSize = pageSize
	}
	objects := make([]*T, 0, findPageSize)
	if err := sess.Find(&objects); err != nil {
		return nil, err
	}
	return objects, nil
}

// Count represents a common count function which accept an options interface
func Count[T any](ctx context.Context, opts FindOptions) (int64, error) {
	sess := GetEngine(ctx)
	if joinOpt, ok := opts.(FindOptionsJoin); ok && len(joinOpt.ToJoins()) > 0 {
		for _, joinFunc := range joinOpt.ToJoins() {
			if err := joinFunc(sess); err != nil {
				return 0, err
			}
		}
	}

	var object T
	return sess.Where(opts.ToConds()).Count(&object)
}

// FindAndCount represents a common findandcount function which accept an options interface
func FindAndCount[T any](ctx context.Context, opts FindOptions) ([]*T, int64, error) {
	sess := GetEngine(ctx).Where(opts.ToConds())
	page, pageSize := opts.GetPage(), opts.GetPageSize()
	if !opts.IsListAll() && pageSize > 0 && page >= 1 {
		sess.Limit(pageSize, (page-1)*pageSize)
	}
	if newOpt, ok := opts.(FindOptionsOrder); ok && newOpt.ToOrders() != "" {
		sess.OrderBy(newOpt.ToOrders())
	}

	findPageSize := defaultFindSliceSize
	if pageSize > 0 {
		findPageSize = pageSize
	}
	objects := make([]*T, 0, findPageSize)
	cnt, err := sess.FindAndCount(&objects)
	if err != nil {
		return nil, 0, err
	}
	return objects, cnt, nil
}