package presets

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"path"
	"strconv"
	"strings"

	"github.com/qor5/admin/presets/actions"
	. "github.com/qor5/ui/vuetify"
	vx "github.com/qor5/ui/vuetifyx"
	"github.com/qor5/web"
	"github.com/qor5/x/i18n"
	"github.com/qor5/x/perm"
	h "github.com/theplant/htmlgo"
)

type ListingBuilder struct {
	mb                *ModelBuilder
	bulkActions       []*BulkActionBuilder
	actions           []*ActionBuilder
	actionsAsMenu     bool
	rowMenu           *RowMenuBuilder
	filterDataFunc    FilterDataFunc
	filterTabsFunc    FilterTabsFunc
	newBtnFunc        ComponentFunc
	pageFunc          web.PageFunc
	cellWrapperFunc   vx.CellWrapperFunc
	Searcher          SearchFunc
	searchColumns     []string
	perPage           int64
	totalVisible      int64
	orderBy           string
	orderableFields   []*OrderableField
	selectableColumns bool
	conditions        []*SQLCondition
	dialogWidth       string
	dialogHeight      string
	FieldsBuilder
}

func (mb *ModelBuilder) Listing(vs ...string) (r *ListingBuilder) {
	r = mb.listing
	if len(vs) == 0 {
		return
	}

	r.Only(vs...)
	return r
}

func (b *ListingBuilder) Only(vs ...string) (r *ListingBuilder) {
	r = b
	ivs := make([]interface{}, 0, len(vs))
	for _, v := range vs {
		ivs = append(ivs, v)
	}
	r.FieldsBuilder = *r.FieldsBuilder.Only(ivs...)
	return
}

func (b *ListingBuilder) PageFunc(pf web.PageFunc) (r *ListingBuilder) {
	b.pageFunc = pf
	return b
}

func (b *ListingBuilder) CellWrapperFunc(cwf vx.CellWrapperFunc) (r *ListingBuilder) {
	b.cellWrapperFunc = cwf
	return b
}

func (b *ListingBuilder) SearchFunc(v SearchFunc) (r *ListingBuilder) {
	b.Searcher = v
	return b
}

func (b *ListingBuilder) SearchColumns(vs ...string) (r *ListingBuilder) {
	b.searchColumns = vs
	return b
}

func (b *ListingBuilder) PerPage(v int64) (r *ListingBuilder) {
	b.perPage = v
	return b
}

func (b *ListingBuilder) TotalVisible(v int64) (r *ListingBuilder) {
	b.totalVisible = v
	return b
}

func (b *ListingBuilder) OrderBy(v string) (r *ListingBuilder) {
	b.orderBy = v
	return b
}

func (b *ListingBuilder) NewButtonFunc(v ComponentFunc) (r *ListingBuilder) {
	b.newBtnFunc = v
	return b
}

func (b *ListingBuilder) ActionsAsMenu(v bool) (r *ListingBuilder) {
	b.actionsAsMenu = v
	return b
}

type OrderableField struct {
	FieldName string
	DBColumn  string
}

func (b *ListingBuilder) OrderableFields(v []*OrderableField) (r *ListingBuilder) {
	b.orderableFields = v
	return b
}

func (b *ListingBuilder) SelectableColumns(v bool) (r *ListingBuilder) {
	b.selectableColumns = v
	return b
}

func (b *ListingBuilder) Conditions(v []*SQLCondition) (r *ListingBuilder) {
	b.conditions = v
	return b
}

func (b *ListingBuilder) DialogWidth(v string) (r *ListingBuilder) {
	b.dialogWidth = v
	return b
}

func (b *ListingBuilder) DialogHeight(v string) (r *ListingBuilder) {
	b.dialogHeight = v
	return b
}

func (b *ListingBuilder) GetPageFunc() web.PageFunc {
	if b.pageFunc != nil {
		return b.pageFunc
	}
	return b.defaultPageFunc
}

const bulkPanelOpenParamName = "bulkOpen"
const actionPanelOpenParamName = "actionOpen"
const DeleteConfirmPortalName = "deleteConfirm"
const dataTablePortalName = "dataTable"
const dataTableAdditionsPortalName = "dataTableAdditions"
const listingDialogContentPortalName = "listingDialogContentPortal"

func (b *ListingBuilder) defaultPageFunc(ctx *web.EventContext) (r web.PageResponse, err error) {
	if b.mb.Info().Verifier().Do(PermList).WithReq(ctx.R).IsAllowed() != nil {
		err = perm.PermissionDenied
		return
	}

	msgr := MustGetMessages(ctx.R)
	title := msgr.ListingObjectTitle(i18n.T(ctx.R, ModelsI18nModuleKey, b.mb.label))
	r.PageTitle = title

	r.Body = b.listingComponent(ctx, false)

	return
}

func (b *ListingBuilder) listingComponent(
	ctx *web.EventContext,
	inDialog bool,
) h.HTMLComponent {
	ctx.R = ctx.R.WithContext(context.WithValue(ctx.R.Context(), ctxInDialog, inDialog))

	msgr := MustGetMessages(ctx.R)

	var tabsAndActionsBar h.HTMLComponent
	{
		filterTabs := b.filterTabs(ctx, inDialog)

		var actionsComponent h.HTMLComponents
		if v := b.actionsComponent(msgr, ctx, inDialog); v != nil {
			actionsComponent = append(actionsComponent, v)
		}
		if b.newBtnFunc != nil {
			if btn := b.newBtnFunc(ctx); btn != nil {
				actionsComponent = append(actionsComponent, b.newBtnFunc(ctx))
			}
		} else {
			disableNewBtn := b.mb.Info().Verifier().Do(PermCreate).WithReq(ctx.R).IsAllowed() != nil
			if !disableNewBtn {
				onclick := web.Plaid().EventFunc(actions.New)
				if inDialog {
					onclick.URL(ctx.R.RequestURI).
						Query(ParamOverlay, actions.Dialog).
						Query(ParamInDialog, true).
						Query(ParamListingQueries, ctx.Queries().Encode())
				}
				actionsComponent = append(actionsComponent, VBtn(msgr.New).
					Color("primary").
					Depressed(true).
					Dark(true).Class("ml-2").
					Disabled(disableNewBtn).
					Attr("@click", onclick.Go()))
			}
		}

		if filterTabs != nil || len(actionsComponent) > 0 {
			tabsAndActionsBar = VToolbar(
				filterTabs,
				VSpacer(),
				actionsComponent,
			).Flat(true)
		}
	}

	var filterBar h.HTMLComponent
	if b.filterDataFunc != nil {
		fd := b.filterDataFunc(ctx)
		fd.SetByQueryString(ctx.R.URL.RawQuery)
		filterBar = b.filterBar(ctx, msgr, fd, inDialog)
	}

	dataTable, dataTableAdditions := b.getTableComponents(ctx, inDialog)

	var dialogHeaderBar h.HTMLComponent
	if inDialog {
		title := msgr.ListingObjectTitle(i18n.T(ctx.R, ModelsI18nModuleKey, b.mb.label))
		var searchBox h.HTMLComponent
		if b.mb.layoutConfig == nil || !b.mb.layoutConfig.SearchBoxInvisible {
			searchBox = VTextField().
				PrependInnerIcon("search").
				Placeholder(msgr.Search).
				HideDetails(true).
				Value(ctx.R.URL.Query().Get("keyword")).
				Attr("@keyup.enter", web.Plaid().
					URL(ctx.R.RequestURI).
					Query("keyword", web.Var("[$event.target.value]")).
					MergeQuery(true).
					EventFunc(actions.UpdateListingDialog).
					Go()).
				Attr("@click:clear", web.Plaid().
					URL(ctx.R.RequestURI).
					Query("keyword", "").
					MergeQuery(true).
					EventFunc(actions.UpdateListingDialog).
					Go()).
				Class("ma-0 pa-0 mr-6")
		}
		dialogHeaderBar = VAppBar(
			VToolbarTitle("").
				Children(h.Text(title)),
			VSpacer(),
			searchBox,
			VBtn("").Icon(true).
				Children(VIcon("close")).
				Large(true).
				Attr("@click.stop", CloseListingDialogVarScript),
		).Color("white").Elevation(0).Dense(true)
	}

	return VContainer(
		dialogHeaderBar,
		tabsAndActionsBar,
		h.Div(
			VCard(
				filterBar,
				VDivider(),
				VCardText(
					web.Portal(dataTable).Name(dataTablePortalName),
				).Class("pa-0"),
			),
			web.Portal(dataTableAdditions).Name(dataTableAdditionsPortalName),
		).Class("mt-2"),
	).Fluid(true).
		Class("white").
		Attr(web.InitContextVars, `{currEditingListItemID: ''}`)
}

func (b *ListingBuilder) cellComponentFunc(f *FieldBuilder) vx.CellComponentFunc {
	return func(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent {
		return f.compFunc(obj, b.mb.getComponentFuncField(f), ctx)
	}
}

func getSelectedIds(ctx *web.EventContext) (selected []string) {
	selectedValue := ctx.R.URL.Query().Get(ParamSelectedIds)
	if len(selectedValue) > 0 {
		selected = strings.Split(selectedValue, ",")
	}
	return selected
}

func (b *ListingBuilder) bulkPanel(
	bulk *BulkActionBuilder,
	selectedIds []string,
	processedSelectedIds []string,
	ctx *web.EventContext,
) (r h.HTMLComponent) {
	msgr := MustGetMessages(ctx.R)

	var errComp h.HTMLComponent
	if vErr, ok := ctx.Flash.(*web.ValidationErrors); ok {
		if gErr := vErr.GetGlobalError(); gErr != "" {
			errComp = VAlert(h.Text(gErr)).
				Border("left").
				Type("error").
				Elevation(2).
				ColoredBorder(true)
		}
	}
	var processSelectedIdsNotice h.HTMLComponent
	if len(processedSelectedIds) < len(selectedIds) {
		unactionables := make([]string, 0, len(selectedIds))
		{
			processedSelectedIdsM := make(map[string]struct{})
			for _, v := range processedSelectedIds {
				processedSelectedIdsM[v] = struct{}{}
			}
			for _, v := range selectedIds {
				if _, ok := processedSelectedIdsM[v]; !ok {
					unactionables = append(unactionables, v)
				}
			}
		}

		if len(unactionables) > 0 {
			var noticeText string
			if bulk.selectedIdsProcessorNoticeFunc != nil {
				noticeText = bulk.selectedIdsProcessorNoticeFunc(selectedIds, processedSelectedIds, unactionables)
			} else {
				var idsText string
				if len(unactionables) <= 10 {
					idsText = strings.Join(unactionables, ", ")
				} else {
					idsText = fmt.Sprintf("%s...(+%d)", strings.Join(unactionables[:10], ", "), len(unactionables)-10)
				}
				noticeText = msgr.BulkActionSelectedIdsProcessNotice(idsText)
			}
			processSelectedIdsNotice = VAlert(h.Text(noticeText)).
				Type("warning")
		}
	}

	onOK := web.Plaid().EventFunc(actions.DoBulkAction).
		Query(ParamBulkActionName, bulk.name).
		MergeQuery(true)
	if isInDialogFromQuery(ctx) {
		onOK.URL(ctx.R.RequestURI)
	}
	return VCard(
		VCardTitle(
			h.Text(bulk.NameLabel.label),
		),
		VCardText(
			errComp,
			processSelectedIdsNotice,
			bulk.compFunc(selectedIds, ctx),
		),
		VCardActions(
			VSpacer(),
			VBtn(msgr.Cancel).
				Depressed(true).
				Class("ml-2").
				Attr("@click", closeDialogVarScript),

			VBtn(msgr.OK).
				Color("primary").
				Depressed(true).
				Dark(true).
				Attr("@click", onOK.Go()),
		),
	)
}

func (b *ListingBuilder) actionPanel(action *ActionBuilder, ctx *web.EventContext) (r h.HTMLComponent) {
	msgr := MustGetMessages(ctx.R)

	var errComp h.HTMLComponent
	if vErr, ok := ctx.Flash.(*web.ValidationErrors); ok {
		if gErr := vErr.GetGlobalError(); gErr != "" {
			errComp = VAlert(h.Text(gErr)).
				Border("left").
				Type("error").
				Elevation(2).
				ColoredBorder(true)
		}
	}

	onOK := web.Plaid().EventFunc(actions.DoListingAction).
		Query(ParamListingActionName, action.name).
		MergeQuery(true)
	if isInDialogFromQuery(ctx) {
		onOK.URL(ctx.R.RequestURI)
	}

	var comp h.HTMLComponent
	if action.compFunc != nil {
		comp = action.compFunc("", ctx)
	}

	return VCard(
		VCardTitle(
			h.Text(action.NameLabel.label),
		),
		VCardText(
			errComp,
			comp,
		),
		VCardActions(
			VSpacer(),
			VBtn(msgr.Cancel).
				Depressed(true).
				Class("ml-2").
				Attr("@click", closeDialogVarScript),

			VBtn(msgr.OK).
				Color("primary").
				Depressed(true).
				Dark(true).
				Attr("@click", onOK.Go()),
		),
	)
}

func (b *ListingBuilder) deleteConfirmation(ctx *web.EventContext) (r web.EventResponse, err error) {
	msgr := MustGetMessages(ctx.R)
	id := ctx.R.FormValue(ParamID)
	promptID := id
	if v := ctx.R.FormValue("prompt_id"); v != "" {
		promptID = v
	}

	r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
		Name: DeleteConfirmPortalName,
		Body: VDialog(
			VCard(
				VCardTitle(h.Text(msgr.DeleteConfirmationText(promptID))),
				VCardActions(
					VSpacer(),
					VBtn(msgr.Cancel).
						Depressed(true).
						Class("ml-2").
						On("click", "vars.deleteConfirmation = false"),

					VBtn(msgr.Delete).
						Color("primary").
						Depressed(true).
						Dark(true).
						Attr("@click", web.Plaid().
							EventFunc(actions.DoDelete).
							Queries(ctx.Queries()).
							URL(ctx.R.URL.Path).
							Go()),
				),
			),
		).MaxWidth("600px").
			Attr("v-model", "vars.deleteConfirmation").
			Attr(web.InitContextVars, `{deleteConfirmation: false}`),
	})

	r.VarsScript = "setTimeout(function(){ vars.deleteConfirmation = true }, 100)"
	return
}

func (b *ListingBuilder) openActionDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
	actionName := ctx.R.URL.Query().Get(actionPanelOpenParamName)
	action := getAction(b.actions, actionName)
	if action == nil {
		err = errors.New("cannot find requested action")
		return
	}

	b.mb.p.dialog(
		&r,
		b.actionPanel(action, ctx),
		action.dialogWidth,
	)
	return
}

func (b *ListingBuilder) openBulkActionDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
	msgr := MustGetMessages(ctx.R)
	selected := getSelectedIds(ctx)
	bulkName := ctx.R.URL.Query().Get(bulkPanelOpenParamName)
	bulk := getBulkAction(b.bulkActions, bulkName)

	if bulk == nil {
		err = errors.New("cannot find requested action")
		return
	}

	if len(selected) == 0 {
		ShowMessage(&r, "Please select record", "warning")
		return
	}

	// If selectedIdsProcessorFunc is not nil, process the request in it and skip the confirmation dialog
	var processedSelectedIds []string
	if bulk.selectedIdsProcessorFunc != nil {
		processedSelectedIds, err = bulk.selectedIdsProcessorFunc(selected, ctx)
		if err != nil {
			return
		}
		if len(processedSelectedIds) == 0 {
			if bulk.selectedIdsProcessorNoticeFunc != nil {
				ShowMessage(&r, bulk.selectedIdsProcessorNoticeFunc(selected, processedSelectedIds, selected), "warning")
			} else {
				ShowMessage(&r, msgr.BulkActionNoAvailableRecords, "warning")
			}
			return
		}
	} else {
		processedSelectedIds = selected
	}

	b.mb.p.dialog(
		&r,
		b.bulkPanel(bulk, selected, processedSelectedIds, ctx),
		bulk.dialogWidth,
	)
	return
}

func (b *ListingBuilder) doBulkAction(ctx *web.EventContext) (r web.EventResponse, err error) {
	bulk := getBulkAction(b.bulkActions, ctx.R.FormValue(ParamBulkActionName))
	if bulk == nil {
		panic("bulk required")
	}

	if b.mb.Info().Verifier().SnakeDo(PermBulkActions, bulk.name).WithReq(ctx.R).IsAllowed() != nil {
		ShowMessage(&r, perm.PermissionDenied.Error(), "warning")
		return
	}

	selectedIds := getSelectedIds(ctx)

	var err1 error
	var processedSelectedIds []string
	if bulk.selectedIdsProcessorFunc != nil {
		processedSelectedIds, err1 = bulk.selectedIdsProcessorFunc(selectedIds, ctx)
	} else {
		processedSelectedIds = selectedIds
	}

	if err1 == nil {
		err1 = bulk.updateFunc(processedSelectedIds, ctx)
	}

	if err1 != nil {
		if _, ok := err1.(*web.ValidationErrors); !ok {
			vErr := &web.ValidationErrors{}
			vErr.GlobalError(err1.Error())
			ctx.Flash = vErr
		}
	}

	if ctx.Flash != nil {
		r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
			Name: dialogContentPortalName,
			Body: b.bulkPanel(bulk, selectedIds, processedSelectedIds, ctx),
		})
		return
	}

	msgr := MustGetMessages(ctx.R)
	ShowMessage(&r, msgr.SuccessfullyUpdated, "")
	if isInDialogFromQuery(ctx) {
		qs := ctx.Queries()
		qs.Del(bulkPanelOpenParamName)
		qs.Del(ParamBulkActionName)
		web.AppendVarsScripts(&r,
			closeDialogVarScript,
			web.Plaid().
				URL(ctx.R.RequestURI).
				EventFunc(actions.UpdateListingDialog).
				Queries(qs).
				Go(),
		)
	} else {
		r.PushState = web.Location(url.Values{bulkPanelOpenParamName: []string{}}).MergeQuery(true)
	}

	return
}

func (b *ListingBuilder) doListingAction(ctx *web.EventContext) (r web.EventResponse, err error) {
	action := getAction(b.actions, ctx.R.FormValue(ParamListingActionName))
	if action == nil {
		panic("action required")
	}

	if b.mb.Info().Verifier().SnakeDo(PermDoListingAction, action.name).WithReq(ctx.R).IsAllowed() != nil {
		ShowMessage(&r, perm.PermissionDenied.Error(), "warning")
		return
	}

	if err := action.updateFunc("", ctx); err != nil {
		if _, ok := err.(*web.ValidationErrors); !ok {
			vErr := &web.ValidationErrors{}
			vErr.GlobalError(err.Error())
			ctx.Flash = vErr
		}
	}

	if ctx.Flash != nil {
		r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
			Name: dialogContentPortalName,
			Body: b.actionPanel(action, ctx),
		})
		return
	}

	msgr := MustGetMessages(ctx.R)
	ShowMessage(&r, msgr.SuccessfullyUpdated, "")

	if isInDialogFromQuery(ctx) {
		qs := ctx.Queries()
		qs.Del(actionPanelOpenParamName)
		qs.Del(ParamListingActionName)
		web.AppendVarsScripts(&r,
			closeDialogVarScript,
			web.Plaid().
				URL(ctx.R.RequestURI).
				EventFunc(actions.UpdateListingDialog).
				Queries(qs).
				Go(),
		)
	} else {
		r.PushState = web.Location(url.Values{actionPanelOpenParamName: []string{}}).MergeQuery(true)
	}

	return
}

const ActiveFilterTabQueryKey = "active_filter_tab"

func (b *ListingBuilder) filterTabs(
	ctx *web.EventContext,
	inDialog bool,
) (r h.HTMLComponent) {
	if b.filterTabsFunc == nil {
		return
	}

	qs := ctx.R.URL.Query()

	tabs := VTabs().ShowArrows(true)
	tabsData := b.filterTabsFunc(ctx)
	for i, tab := range tabsData {
		if tab.ID == "" {
			tab.ID = fmt.Sprintf("tab%d", i)
		}
	}
	value := -1
	activeTabValue := qs.Get(ActiveFilterTabQueryKey)

	for i, td := range tabsData {
		// Find selected tab by active_filter_tab=xx in the url query
		if activeTabValue == td.ID {
			value = i
		}

		tabContent := h.Text(td.Label)
		if td.AdvancedLabel != nil {
			tabContent = td.AdvancedLabel
		}

		totalQuery := url.Values{}
		totalQuery.Set(ActiveFilterTabQueryKey, td.ID)
		for k, v := range td.Query {
			totalQuery[k] = v
		}

		onclick := web.Plaid().Queries(totalQuery)
		if inDialog {
			onclick.URL(ctx.R.RequestURI).
				EventFunc(actions.UpdateListingDialog)
		} else {
			onclick.PushState(true)
		}
		tabs.AppendChildren(
			VTab(tabContent).
				Attr("@click", onclick.Go()),
		)
	}
	return tabs.Value(value)
}

type selectColumns struct {
	DisplayColumns []string       `json:"displayColumns,omitempty"`
	SortedColumns  []sortedColumn `json:"sortedColumns,omitempty"`
}
type sortedColumn struct {
	Name  string `json:"name"`
	Label string `json:"label"`
}

func (b *ListingBuilder) selectColumnsBtn(
	pageURL *url.URL,
	ctx *web.EventContext,
	inDialog bool,
) (btn h.HTMLComponent, displaySortedFields []*FieldBuilder) {
	var (
		_, respath         = path.Split(pageURL.Path)
		displayColumnsName = fmt.Sprintf("%s_display_columns", respath)
		sortedColumnsName  = fmt.Sprintf("%s_sorted_columns", respath)
		originalColumns    []string
		displayColumns     []string
		sortedColumns      []string
	)

	for _, f := range b.fields {
		if b.mb.Info().Verifier().Do(PermList).SnakeOn("f_"+f.name).WithReq(ctx.R).IsAllowed() != nil {
			continue
		}
		originalColumns = append(originalColumns, f.name)
	}

	// get the columns setting from url params or cookie data
	if urldata := pageURL.Query().Get(displayColumnsName); urldata != "" {
		if urlColumns := strings.Split(urldata, ","); len(urlColumns) > 0 {
			displayColumns = urlColumns
		}
	}

	if urldata := pageURL.Query().Get(sortedColumnsName); urldata != "" {
		if urlColumns := strings.Split(urldata, ","); len(urlColumns) > 0 {
			sortedColumns = urlColumns
		}
	}

	// get the columns setting from  cookie data
	if len(displayColumns) == 0 {
		cookiedata, err := ctx.R.Cookie(displayColumnsName)
		if err == nil {
			if cookieColumns := strings.Split(cookiedata.Value, ","); len(cookieColumns) > 0 {
				displayColumns = cookieColumns
			}
		}
	}

	if len(sortedColumns) == 0 {
		cookiedata, err := ctx.R.Cookie(sortedColumnsName)
		if err == nil {
			if cookieColumns := strings.Split(cookiedata.Value, ","); len(cookieColumns) > 0 {
				sortedColumns = cookieColumns
			}
		}
	}

	// check if listing fileds is changed. if yes, use the original columns
	var originalFiledsChanged bool

	if len(sortedColumns) > 0 && len(originalColumns) != len(sortedColumns) {
		originalFiledsChanged = true
	}

	if len(sortedColumns) > 0 && !originalFiledsChanged {
		for _, sortedColumn := range sortedColumns {
			var find bool
			for _, originalColumn := range originalColumns {
				if sortedColumn == originalColumn {
					find = true
					break
				}
			}
			if !find {
				originalFiledsChanged = true
				break
			}
		}
	}

	if len(displayColumns) > 0 && !originalFiledsChanged {
		for _, displayColumn := range displayColumns {
			var find bool
			for _, originalColumn := range originalColumns {
				if displayColumn == originalColumn {
					find = true
					break
				}
			}
			if !find {
				originalFiledsChanged = true
				break
			}
		}
	}

	// save display columns setting on cookie
	if !originalFiledsChanged && len(displayColumns) > 0 {
		http.SetCookie(ctx.W, &http.Cookie{
			Name:  displayColumnsName,
			Value: strings.Join(displayColumns, ","),
		})
	}

	// save sorted columns setting on cookie
	if !originalFiledsChanged && len(sortedColumns) > 0 {
		http.SetCookie(ctx.W, &http.Cookie{
			Name:  sortedColumnsName,
			Value: strings.Join(sortedColumns, ","),
		})
	}

	// set the data for displaySortedFields on data table
	if originalFiledsChanged || (len(sortedColumns) == 0 && len(displayColumns) == 0) {
		displaySortedFields = b.fields
	}

	if originalFiledsChanged || len(displayColumns) == 0 {
		displayColumns = originalColumns
	}

	if originalFiledsChanged || len(sortedColumns) == 0 {
		sortedColumns = originalColumns
	}

	if len(displaySortedFields) == 0 {
		for _, sortedColumn := range sortedColumns {
			for _, displayColumn := range displayColumns {
				if sortedColumn == displayColumn {
					displaySortedFields = append(displaySortedFields, b.Field(sortedColumn))
					break
				}
			}
		}
	}

	// set the data for selected columns on toolbar
	selectColumns := selectColumns{
		DisplayColumns: displayColumns,
	}
	for _, sc := range sortedColumns {
		selectColumns.SortedColumns = append(selectColumns.SortedColumns, sortedColumn{
			Name:  sc,
			Label: i18n.PT(ctx.R, ModelsI18nModuleKey, b.mb.label, b.mb.getLabel(b.Field(sc).NameLabel)),
		})
	}

	msgr := MustGetMessages(ctx.R)
	onOK := web.Plaid().
		Query(displayColumnsName, web.Var("locals.displayColumns")).
		Query(sortedColumnsName, web.Var("locals.sortedColumns.map(column => column.name )")).
		MergeQuery(true)
	if inDialog {
		onOK.URL(ctx.R.RequestURI).
			EventFunc(actions.UpdateListingDialog)
	}
	// add the HTML component of columns setting into toolbar
	btn = VMenu(
		web.Slot(
			VBtn("").Children(VIcon("settings")).Attr("v-on", "on").Text(true).Fab(true).Small(true),
		).Name("activator").Scope("{ on }"),

		web.Scope(VList(
			h.Tag("vx-draggable").Attr("v-model", "locals.sortedColumns", "draggable", ".vx_column_item", "animation", "300").Children(
				h.Div(
					VListItem(
						VListItemContent(
							VListItemTitle(
								VSwitch().Dense(true).Attr("v-model", "locals.displayColumns", ":value", "column.name", ":label", "column.label", "@click", "event.preventDefault()"),
							),
						),
						VListItemIcon(
							VIcon("reorder"),
						).Attr("style", "margin-top: 28px"),
					),
					VDivider(),
				).Attr("v-for", "(column, index) in locals.sortedColumns", ":key", "column.name", "class", "vx_column_item"),
			),
			VListItem(
				VListItemAction(VBtn(msgr.Cancel).Elevation(0).Attr("@click", `vars.selectColumnsMenu = false`)),
				VListItemAction(VBtn(msgr.OK).Elevation(0).Color("primary").Attr("@click", `vars.selectColumnsMenu = false;`+onOK.Go()))),
		).Dense(true)).
			Init(h.JSONString(selectColumns)).
			VSlot("{ locals }"),
	).OffsetY(true).CloseOnClick(false).CloseOnContentClick(false).
		Attr(web.InitContextVars, `{selectColumnsMenu: false}`).
		Attr("v-model", "vars.selectColumnsMenu")
	return
}

func (b *ListingBuilder) filterBar(
	ctx *web.EventContext,
	msgr *Messages,
	fd vx.FilterData,
	inDialog bool,
) (filterBar h.HTMLComponent) {
	if fd == nil {
		return nil
	}
	noVisiableItem := true
	for _, d := range fd {
		if !d.Invisible {
			noVisiableItem = false
			break
		}
	}
	if noVisiableItem {
		return nil
	}

	ft := vx.FilterTranslations{}
	ft.Clear = msgr.FiltersClear
	ft.Add = msgr.FiltersAdd
	ft.Apply = msgr.FilterApply
	for _, d := range fd {
		d.Translations = vx.FilterIndependentTranslations{
			FilterBy: msgr.FilterBy(d.Label),
		}
	}

	ft.Date.To = msgr.FiltersDateTo

	ft.Number.And = msgr.FiltersNumberAnd
	ft.Number.Equals = msgr.FiltersNumberEquals
	ft.Number.Between = msgr.FiltersNumberBetween
	ft.Number.GreaterThan = msgr.FiltersNumberGreaterThan
	ft.Number.LessThan = msgr.FiltersNumberLessThan

	ft.String.Equals = msgr.FiltersStringEquals
	ft.String.Contains = msgr.FiltersStringContains

	ft.MultipleSelect.In = msgr.FiltersMultipleSelectIn
	ft.MultipleSelect.NotIn = msgr.FiltersMultipleSelectNotIn

	filter := vx.VXFilter(fd).Translations(ft)
	if inDialog {
		filter.OnChange(web.Plaid().
			URL(ctx.R.RequestURI).
			StringQuery(web.Var("$event.encodedFilterData")).
			ClearMergeQuery(web.Var("$event.filterKeys")).
			EventFunc(actions.UpdateListingDialog).
			Go())
	}
	return VToolbar(
		filter,
	).Flat(true).AutoHeight(true).Class("py-2")
}

func getLocalPerPage(
	ctx *web.EventContext,
	mb *ModelBuilder,
) int64 {
	c, err := ctx.R.Cookie("_perPage")
	if err != nil {
		return 0
	}
	vals := strings.Split(c.Value, "$")
	for _, v := range vals {
		vvs := strings.Split(v, "#")
		if len(vvs) != 2 {
			continue
		}
		if vvs[0] == mb.uriName {
			r, _ := strconv.ParseInt(vvs[1], 10, 64)
			return r
		}
	}

	return 0
}

func setLocalPerPage(
	ctx *web.EventContext,
	mb *ModelBuilder,
	v int64,
) {
	var oldVals []string
	{
		c, err := ctx.R.Cookie("_perPage")
		if err == nil {
			oldVals = strings.Split(c.Value, "$")
		}
	}
	newVals := []string{fmt.Sprintf("%s#%d", mb.uriName, v)}
	for _, v := range oldVals {
		vvs := strings.Split(v, "#")
		if len(vvs) != 2 {
			continue
		}
		if vvs[0] == mb.uriName {
			continue
		}
		newVals = append(newVals, v)
	}
	http.SetCookie(ctx.W, &http.Cookie{
		Name:  "_perPage",
		Value: strings.Join(newVals, "$"),
	})
}

type ColOrderBy struct {
	FieldName string
	// ASC, DESC
	OrderBy string
}

func GetOrderBysFromQuery(query url.Values) []*ColOrderBy {
	r := make([]*ColOrderBy, 0)
	qs := strings.Split(query.Get("order_by"), ",")
	for _, q := range qs {
		ss := strings.Split(q, "_")
		ssl := len(ss)
		if ssl == 1 {
			continue
		}
		if ss[ssl-1] != "ASC" && ss[ssl-1] != "DESC" {
			continue
		}
		r = append(r, &ColOrderBy{
			FieldName: strings.Join(ss[:ssl-1], "_"),
			OrderBy:   ss[ssl-1],
		})
	}

	return r
}

func newQueryWithFieldToggleOrderBy(query url.Values, fieldName string) url.Values {
	oldOrderBys := GetOrderBysFromQuery(query)
	newOrderBysQueryValue := []string{}
	existed := false
	for _, oob := range oldOrderBys {
		if oob.FieldName == fieldName {
			existed = true
			if oob.OrderBy == "ASC" {
				newOrderBysQueryValue = append(newOrderBysQueryValue, oob.FieldName+"_DESC")
			}
			continue
		}
		newOrderBysQueryValue = append(newOrderBysQueryValue, oob.FieldName+"_"+oob.OrderBy)
	}
	if !existed {
		newOrderBysQueryValue = append(newOrderBysQueryValue, fieldName+"_ASC")
	}

	newQuery := make(url.Values)
	for k, v := range query {
		newQuery[k] = v
	}
	newQuery.Set("order_by", strings.Join(newOrderBysQueryValue, ","))
	return newQuery
}

func (b *ListingBuilder) getTableComponents(
	ctx *web.EventContext,
	inDialog bool,
) (
	dataTable h.HTMLComponent,
	// pagination, no-record message
	datatableAdditions h.HTMLComponent,
) {
	msgr := MustGetMessages(ctx.R)

	qs := ctx.R.URL.Query()

	var requestPerPage int64
	qPerPageStr := qs.Get("per_page")
	qPerPage, _ := strconv.ParseInt(qPerPageStr, 10, 64)
	if qPerPage != 0 {
		setLocalPerPage(ctx, b.mb, qPerPage)
		requestPerPage = qPerPage
	} else if cPerPage := getLocalPerPage(ctx, b.mb); cPerPage != 0 {
		requestPerPage = cPerPage
	}
	perPage := b.perPage
	if requestPerPage != 0 {
		perPage = requestPerPage
	}
	if perPage == 0 {
		perPage = 50
	}
	if perPage > 1000 {
		perPage = 1000
	}

	totalVisible := b.totalVisible
	if totalVisible == 0 {
		totalVisible = 10
	}

	var orderBySQL string
	orderBys := GetOrderBysFromQuery(qs)
	// map[FieldName]DBColumn
	orderableFieldMap := make(map[string]string)
	for _, v := range b.orderableFields {
		orderableFieldMap[v.FieldName] = v.DBColumn
	}
	for _, ob := range orderBys {
		dbCol, ok := orderableFieldMap[ob.FieldName]
		if !ok {
			continue
		}
		orderBySQL += fmt.Sprintf("%s %s,", dbCol, ob.OrderBy)
	}
	if orderBySQL != "" {
		orderBySQL = orderBySQL[:len(orderBySQL)-1]
	}
	if orderBySQL == "" {
		if b.orderBy != "" {
			orderBySQL = b.orderBy
		} else {
			orderBySQL = fmt.Sprintf("%s DESC", b.mb.primaryField)
		}
	}
	searchParams := &SearchParams{
		KeywordColumns: b.searchColumns,
		Keyword:        qs.Get("keyword"),
		PerPage:        perPage,
		OrderBy:        orderBySQL,
		PageURL:        ctx.R.URL,
		SQLConditions:  b.conditions,
	}

	searchParams.Page, _ = strconv.ParseInt(qs.Get("page"), 10, 64)
	if searchParams.Page == 0 {
		searchParams.Page = 1
	}

	var fd vx.FilterData
	if b.filterDataFunc != nil {
		fd = b.filterDataFunc(ctx)
		cond, args := fd.SetByQueryString(ctx.R.URL.RawQuery)

		searchParams.SQLConditions = append(searchParams.SQLConditions, &SQLCondition{
			Query: cond,
			Args:  args,
		})
	}

	if b.Searcher == nil || b.mb.p.dataOperator == nil {
		panic("presets.New().DataOperator(...) required")
	}

	var objs interface{}
	var totalCount int
	var err error

	objs, totalCount, err = b.Searcher(b.mb.NewModelSlice(), searchParams, ctx)

	if err != nil {
		panic(err)
	}

	haveCheckboxes := len(b.bulkActions) > 0

	pagesCount := int(int64(totalCount)/searchParams.PerPage + 1)
	if int64(totalCount)%searchParams.PerPage == 0 {
		pagesCount--
	}

	var cellWraperFunc = func(cell h.MutableAttrHTMLComponent, id string, obj interface{}, dataTableID string) h.HTMLComponent {
		tdbind := cell
		if b.mb.hasDetailing && !b.mb.detailing.drawer {
			tdbind.SetAttr("@click.self", web.Plaid().
				PushStateURL(
					b.mb.Info().
						DetailingHref(id)).
				Go())
		} else {
			event := actions.Edit
			if b.mb.hasDetailing {
				event = actions.DetailingDrawer
			}
			onclick := web.Plaid().
				EventFunc(event).
				Query(ParamID, id)
			if inDialog {
				onclick.URL(ctx.R.RequestURI).
					Query(ParamOverlay, actions.Dialog).
					Query(ParamInDialog, true).
					Query(ParamListingQueries, ctx.Queries().Encode())
			}
			tdbind.SetAttr("@click.self",
				onclick.Go()+fmt.Sprintf(`; vars.currEditingListItemID="%s-%s"`, dataTableID, id))
		}
		return tdbind
	}
	if b.cellWrapperFunc != nil {
		cellWraperFunc = b.cellWrapperFunc
	}

	var displayFields = b.fields
	var selectColumnsBtn h.HTMLComponent
	if b.selectableColumns {
		selectColumnsBtn, displayFields = b.selectColumnsBtn(ctx.R.URL, ctx, inDialog)
	}

	sDataTable := vx.DataTable(objs).
		CellWrapperFunc(cellWraperFunc).
		HeadCellWrapperFunc(func(cell h.MutableAttrHTMLComponent, field string, title string) h.HTMLComponent {
			if _, ok := orderableFieldMap[field]; ok {
				var orderBy string
				var orderByIdx int
				for i, ob := range orderBys {
					if ob.FieldName == field {
						orderBy = ob.OrderBy
						orderByIdx = i + 1
						break
					}
				}
				th := h.Th("").Style("cursor: pointer; white-space: nowrap;").
					Children(
						h.Span(title).
							Style("text-decoration: underline;"),
						h.If(orderBy == "ASC",
							VIcon("arrow_drop_up").Small(true),
							h.Span(fmt.Sprint(orderByIdx)),
						).ElseIf(orderBy == "DESC",
							VIcon("arrow_drop_down").Small(true),
							h.Span(fmt.Sprint(orderByIdx)),
						).Else(
							// take up place
							h.Span("").Style("visibility: hidden;").Children(
								VIcon("arrow_drop_down").Small(true),
								h.Span(fmt.Sprint(orderByIdx)),
							),
						),
					)
				qs.Del("__execute_event__")
				newQuery := newQueryWithFieldToggleOrderBy(qs, field)
				onclick := web.Plaid().
					Queries(newQuery)
				if inDialog {
					onclick.URL(ctx.R.RequestURI).
						EventFunc(actions.UpdateListingDialog)
				} else {
					onclick.PushState(true)
				}
				th.Attr("@click", onclick.Go())

				cell = th
			}

			return cell
		}).
		RowWrapperFunc(func(row h.MutableAttrHTMLComponent, id string, obj interface{}, dataTableID string) h.HTMLComponent {
			row.SetAttr(":class", fmt.Sprintf(`{"vx-list-item--active primary--text": vars.presetsRightDrawer && vars.currEditingListItemID==="%s-%s"}`, dataTableID, id))
			return row
		}).
		RowMenuItemFuncs(b.RowMenu().listingItemFuncs(ctx)...).
		Selectable(haveCheckboxes).
		SelectionParamName(ParamSelectedIds).
		SelectedCountLabel(msgr.ListingSelectedCountNotice).
		SelectableColumnsBtn(selectColumnsBtn).
		ClearSelectionLabel(msgr.ListingClearSelection)
	if inDialog {
		sDataTable.OnSelectAllFunc(func(idsOfPage []string, ctx *web.EventContext) string {
			return web.Plaid().
				URL(ctx.R.RequestURI).
				EventFunc(actions.UpdateListingDialog).
				Query(ParamSelectedIds,
					web.Var(fmt.Sprintf(`{value: %s, add: $event, remove: !$event}`, h.JSONString(idsOfPage))),
				).
				MergeQuery(true).
				Go()
		})
		sDataTable.OnSelectFunc(func(id string, ctx *web.EventContext) string {
			return web.Plaid().
				URL(ctx.R.RequestURI).
				EventFunc(actions.UpdateListingDialog).
				Query(ParamSelectedIds,
					web.Var(fmt.Sprintf(`{value: %s, add: $event, remove: !$event}`, h.JSONString(id))),
				).
				MergeQuery(true).
				Go()
		})
		sDataTable.OnClearSelectionFunc(func(ctx *web.EventContext) string {
			return web.Plaid().
				URL(ctx.R.RequestURI).
				EventFunc(actions.UpdateListingDialog).
				Query(ParamSelectedIds, "").
				MergeQuery(true).
				Go()
		})
	}
	dataTable = sDataTable

	for _, f := range displayFields {
		if b.mb.Info().Verifier().Do(PermList).SnakeOn("f_"+f.name).WithReq(ctx.R).IsAllowed() != nil {
			continue
		}
		f = b.getFieldOrDefault(f.name) // fill in empty compFunc and setter func with default
		dataTable.(*vx.DataTableBuilder).Column(f.name).
			Title(i18n.PT(ctx.R, ModelsI18nModuleKey, b.mb.label, b.mb.getLabel(f.NameLabel))).
			CellComponentFunc(b.cellComponentFunc(f))
	}

	if totalCount > 0 {
		tpb := vx.VXTablePagination().
			Total(int64(totalCount)).
			CurrPage(searchParams.Page).
			PerPage(searchParams.PerPage).
			CustomPerPages([]int64{b.perPage}).
			PerPageText(msgr.PaginationRowsPerPage)

		if inDialog {
			tpb.OnSelectPerPage(web.Plaid().
				URL(ctx.R.RequestURI).
				Query("per_page", web.Var("[$event]")).
				MergeQuery(true).
				EventFunc(actions.UpdateListingDialog).
				Go())
			tpb.OnPrevPage(web.Plaid().
				URL(ctx.R.RequestURI).
				Query("page", searchParams.Page-1).
				MergeQuery(true).
				EventFunc(actions.UpdateListingDialog).
				Go())
			tpb.OnNextPage(web.Plaid().
				URL(ctx.R.RequestURI).
				Query("page", searchParams.Page+1).
				MergeQuery(true).
				EventFunc(actions.UpdateListingDialog).
				Go())
		}

		datatableAdditions = tpb
	} else {
		datatableAdditions = h.Div(h.Text(msgr.ListingNoRecordToShow)).Class("mt-10 text-center grey--text text--darken-2")
	}

	return
}

func (b *ListingBuilder) reloadList(ctx *web.EventContext) (r web.EventResponse, err error) {
	dataTable, dataTableAdditions := b.getTableComponents(ctx, false)
	r.UpdatePortals = append(r.UpdatePortals,
		&web.PortalUpdate{
			Name: dataTablePortalName,
			Body: dataTable,
		},
		&web.PortalUpdate{
			Name: dataTableAdditionsPortalName,
			Body: dataTableAdditions,
		},
	)

	return
}

func (b *ListingBuilder) actionsComponent(
	msgr *Messages,
	ctx *web.EventContext,
	inDialog bool,
) h.HTMLComponent {
	var actionBtns []h.HTMLComponent

	// Render bulk actions
	for _, ba := range b.bulkActions {
		if b.mb.Info().Verifier().SnakeDo(PermBulkActions, ba.name).WithReq(ctx.R).IsAllowed() != nil {
			continue
		}

		var btn h.HTMLComponent
		if ba.buttonCompFunc != nil {
			btn = ba.buttonCompFunc(ctx)
		} else {
			buttonColor := ba.buttonColor
			if buttonColor == "" {
				buttonColor = ColorSecondary
			}
			onclick := web.Plaid().EventFunc(actions.OpenBulkActionDialog).
				Queries(url.Values{bulkPanelOpenParamName: []string{ba.name}}).
				MergeQuery(true)
			if inDialog {
				onclick.URL(ctx.R.RequestURI).
					Query(ParamInDialog, inDialog)
			}
			btn = VBtn(b.mb.getLabel(ba.NameLabel)).
				Color(buttonColor).
				Depressed(true).
				Dark(true).
				Class("ml-2").
				Attr("@click", onclick.Go())
		}

		actionBtns = append(actionBtns, btn)
	}

	// Render actions
	for _, ba := range b.actions {
		if b.mb.Info().Verifier().SnakeDo(PermActions, ba.name).WithReq(ctx.R).IsAllowed() != nil {
			continue
		}

		var btn h.HTMLComponent
		if ba.buttonCompFunc != nil {
			btn = ba.buttonCompFunc(ctx)
		} else {
			buttonColor := ba.buttonColor
			if buttonColor == "" {
				buttonColor = ColorPrimary
			}

			onclick := web.Plaid().EventFunc(actions.OpenActionDialog).
				Queries(url.Values{actionPanelOpenParamName: []string{ba.name}}).
				MergeQuery(true)
			if inDialog {
				onclick.URL(ctx.R.RequestURI).
					Query(ParamInDialog, inDialog)
			}
			btn = VBtn(b.mb.getLabel(ba.NameLabel)).
				Color(buttonColor).
				Depressed(true).
				Dark(true).
				Class("ml-2").
				Attr("@click", onclick.Go())
		}

		actionBtns = append(actionBtns, btn)
	}

	if len(actionBtns) == 0 {
		return nil
	}

	if b.actionsAsMenu {
		var listItems []h.HTMLComponent
		for _, btn := range actionBtns {
			listItems = append(listItems, VListItem(btn))
		}
		return h.Components(VMenu(
			web.Slot(
				VBtn("Actions").
					Attr("v-bind", "attrs").
					Attr("v-on", "on"),
			).Name("activator").Scope("{ on, attrs }"),
			VList(listItems...),
		).OpenOnHover(true).
			OffsetY(true).
			AllowOverflow(true))
	}

	return h.Components(actionBtns...)
}

func (b *ListingBuilder) openListingDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
	content := VCard(
		web.Portal(b.listingComponent(ctx, true)).
			Name(listingDialogContentPortalName),
	).Attr("id", "listingDialog")
	dialog := VDialog(content).
		Attr("v-model", "vars.presetsListingDialog")
	if b.dialogWidth != "" {
		dialog.Width(b.dialogWidth)
	}
	if b.dialogHeight != "" {
		content.Attr("height", b.dialogHeight)
	}
	r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
		Name: ListingDialogPortalName,
		Body: web.Scope(dialog).VSlot("{ plaidForm }"),
	})
	r.VarsScript = "setTimeout(function(){ vars.presetsListingDialog = true }, 100)"
	return
}

func (b *ListingBuilder) updateListingDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
	r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
		Name: listingDialogContentPortalName,
		Body: b.listingComponent(ctx, true),
	})

	web.AppendVarsScripts(&r, `
var listingDialogElem = document.getElementById('listingDialog'); 
if (listingDialogElem.offsetHeight > parseInt(listingDialogElem.style.minHeight || '0', 10)) {
    listingDialogElem.style.minHeight = listingDialogElem.offsetHeight+'px';
};`)

	return
}