// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// 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 <https://www.gnu.org/licenses/>.

package model

import (
	"errors"
	"fmt"
	"path"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/88250/gulu"
	"github.com/emirpasic/gods/sets/hashset"
	"github.com/siyuan-note/logging"
	"github.com/siyuan-note/siyuan/kernel/bazaar"
	"github.com/siyuan-note/siyuan/kernel/task"
	"github.com/siyuan-note/siyuan/kernel/util"
	"golang.org/x/mod/semver"
)

func BatchUpdateBazaarPackages(frontend string) {
	plugins, widgets, icons, themes, templates := UpdatedPackages(frontend)

	total := len(plugins) + len(widgets) + len(icons) + len(themes) + len(templates)
	if 1 > total {
		return
	}

	util.PushEndlessProgress(fmt.Sprintf(Conf.language(235), 1, total))
	defer util.PushClearProgress()
	count := 1
	for _, plugin := range plugins {
		err := bazaar.InstallPlugin(plugin.RepoURL, plugin.RepoHash, filepath.Join(util.DataDir, "plugins", plugin.Name), Conf.System.ID)
		if err != nil {
			logging.LogErrorf("update plugin [%s] failed: %s", plugin.Name, err)
			util.PushErrMsg(fmt.Sprintf(Conf.language(238), plugin.Name), 5000)
			return
		}

		count++
		util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, plugin.Name))
	}

	for _, widget := range widgets {
		err := bazaar.InstallWidget(widget.RepoURL, widget.RepoHash, filepath.Join(util.DataDir, "widgets", widget.Name), Conf.System.ID)
		if err != nil {
			logging.LogErrorf("update widget [%s] failed: %s", widget.Name, err)
			util.PushErrMsg(fmt.Sprintf(Conf.language(238), widget.Name), 5000)
			return
		}

		count++
		util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, widget.Name))
	}

	for _, icon := range icons {
		err := bazaar.InstallIcon(icon.RepoURL, icon.RepoHash, filepath.Join(util.IconsPath, icon.Name), Conf.System.ID)
		if err != nil {
			logging.LogErrorf("update icon [%s] failed: %s", icon.Name, err)
			util.PushErrMsg(fmt.Sprintf(Conf.language(238), icon.Name), 5000)
			return
		}

		count++
		util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, icon.Name))
	}

	for _, template := range templates {
		err := bazaar.InstallTemplate(template.RepoURL, template.RepoHash, filepath.Join(util.DataDir, "templates", template.Name), Conf.System.ID)
		if err != nil {
			logging.LogErrorf("update template [%s] failed: %s", template.Name, err)
			util.PushErrMsg(fmt.Sprintf(Conf.language(238), template.Name), 5000)
			return
		}

		count++
		util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, template.Name))
	}

	for _, theme := range themes {
		err := bazaar.InstallTheme(theme.RepoURL, theme.RepoHash, filepath.Join(util.ThemesPath, theme.Name), Conf.System.ID)
		if err != nil {
			logging.LogErrorf("update theme [%s] failed: %s", theme.Name, err)
			util.PushErrMsg(fmt.Sprintf(Conf.language(238), theme.Name), 5000)
			return
		}

		count++
		util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, theme.Name))
	}

	util.ReloadUI()
	task.AppendAsyncTaskWithDelay(task.PushMsg, 3*time.Second, util.PushMsg, fmt.Sprintf(Conf.language(237), total), 5000)
	return
}

func UpdatedPackages(frontend string) (plugins []*bazaar.Plugin, widgets []*bazaar.Widget, icons []*bazaar.Icon, themes []*bazaar.Theme, templates []*bazaar.Template) {
	wg := &sync.WaitGroup{}
	wg.Add(5)
	go func() {
		defer wg.Done()
		tmp := InstalledPlugins(frontend, "")
		for _, plugin := range tmp {
			if plugin.Outdated {
				plugins = append(plugins, plugin)
			}
			plugin.PreferredReadme = "" // 清空这个字段，前端会请求在线的 README
		}
	}()

	go func() {
		defer wg.Done()
		tmp := InstalledWidgets("")
		for _, widget := range tmp {
			if widget.Outdated {
				widgets = append(widgets, widget)
			}
			widget.PreferredReadme = ""
		}
	}()

	go func() {
		defer wg.Done()
		tmp := InstalledIcons("")
		for _, icon := range tmp {
			if icon.Outdated {
				icons = append(icons, icon)
			}
			icon.PreferredReadme = ""
		}
	}()

	go func() {
		defer wg.Done()
		tmp := InstalledThemes("")
		for _, theme := range tmp {
			if theme.Outdated {
				themes = append(themes, theme)
			}
			theme.PreferredReadme = ""
		}
	}()

	go func() {
		defer wg.Done()
		tmp := InstalledTemplates("")
		for _, template := range tmp {
			if template.Outdated {
				templates = append(templates, template)
			}
			template.PreferredReadme = ""
		}
	}()

	wg.Wait()

	if 1 > len(plugins) {
		plugins = []*bazaar.Plugin{}
	}

	if 1 > len(widgets) {
		widgets = []*bazaar.Widget{}
	}

	if 1 > len(icons) {
		icons = []*bazaar.Icon{}
	}

	if 1 > len(themes) {
		themes = []*bazaar.Theme{}
	}

	if 1 > len(templates) {
		templates = []*bazaar.Template{}
	}
	return
}

func GetPackageREADME(repoURL, repoHash, packageType string) (ret string) {
	ret = bazaar.GetPackageREADME(repoURL, repoHash, packageType)
	return
}

func BazaarPlugins(frontend, keyword string) (plugins []*bazaar.Plugin) {
	plugins = bazaar.Plugins(frontend)
	plugins = filterPlugins(plugins, keyword)
	for _, plugin := range plugins {
		plugin.Installed = util.IsPathRegularDirOrSymlinkDir(filepath.Join(util.DataDir, "plugins", plugin.Name))
		if plugin.Installed {
			if pluginConf, err := bazaar.PluginJSON(plugin.Name); err == nil && nil != plugin {
				plugin.Outdated = 0 > semver.Compare("v"+pluginConf.Version, "v"+plugin.Version)
			}
		} else {
			plugin.Outdated = false
		}
	}
	return
}

func filterPlugins(plugins []*bazaar.Plugin, keyword string) (ret []*bazaar.Plugin) {
	keywords := getSearchKeywords(keyword)
	if 0 == len(keywords) {
		return plugins
	}
	ret = []*bazaar.Plugin{}
	for _, plugin := range plugins {
		if matchPackage(keywords, plugin.Package) {
			ret = append(ret, plugin)
		}
	}
	return
}

func InstalledPlugins(frontend, keyword string) (plugins []*bazaar.Plugin) {
	plugins = bazaar.InstalledPlugins(frontend)
	plugins = filterPlugins(plugins, keyword)
	petals := getPetals()
	for _, plugin := range plugins {
		petal := getPetalByName(plugin.Name, petals)
		if nil != petal {
			plugin.Enabled = petal.Enabled
		}
	}
	return
}

func InstallBazaarPlugin(repoURL, repoHash, pluginName string) error {
	installPath := filepath.Join(util.DataDir, "plugins", pluginName)
	err := bazaar.InstallPlugin(repoURL, repoHash, installPath, Conf.System.ID)
	if err != nil {
		return errors.New(fmt.Sprintf(Conf.Language(46), pluginName, err))
	}
	return nil
}

func UninstallBazaarPlugin(pluginName, frontend string) error {
	installPath := filepath.Join(util.DataDir, "plugins", pluginName)
	err := bazaar.UninstallPlugin(installPath)
	if err != nil {
		return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
	}

	petals := getPetals()
	var tmp []*Petal
	for i, petal := range petals {
		if petal.Name != pluginName {
			tmp = append(tmp, petals[i])
		}
	}
	petals = tmp
	savePetals(petals)

	uninstallPluginSet := hashset.New(pluginName)
	PushReloadPlugin(nil, nil, nil, uninstallPluginSet, "")
	return nil
}

func BazaarWidgets(keyword string) (widgets []*bazaar.Widget) {
	widgets = bazaar.Widgets()
	widgets = filterWidgets(widgets, keyword)
	for _, widget := range widgets {
		widget.Installed = util.IsPathRegularDirOrSymlinkDir(filepath.Join(util.DataDir, "widgets", widget.Name))
		if widget.Installed {
			if widgetConf, err := bazaar.WidgetJSON(widget.Name); err == nil && nil != widget {
				widget.Outdated = 0 > semver.Compare("v"+widgetConf.Version, "v"+widget.Version)
			}
		} else {
			widget.Outdated = false
		}
	}
	return
}

func filterWidgets(widgets []*bazaar.Widget, keyword string) (ret []*bazaar.Widget) {
	keywords := getSearchKeywords(keyword)
	if 0 == len(keywords) {
		return widgets
	}
	ret = []*bazaar.Widget{}
	for _, w := range widgets {
		if matchPackage(keywords, w.Package) {
			ret = append(ret, w)
		}
	}
	return
}

func InstalledWidgets(keyword string) (widgets []*bazaar.Widget) {
	widgets = bazaar.InstalledWidgets()
	widgets = filterWidgets(widgets, keyword)
	return
}

func InstallBazaarWidget(repoURL, repoHash, widgetName string) error {
	installPath := filepath.Join(util.DataDir, "widgets", widgetName)
	err := bazaar.InstallWidget(repoURL, repoHash, installPath, Conf.System.ID)
	if err != nil {
		return errors.New(fmt.Sprintf(Conf.Language(46), widgetName, err))
	}
	return nil
}

func UninstallBazaarWidget(widgetName string) error {
	installPath := filepath.Join(util.DataDir, "widgets", widgetName)
	err := bazaar.UninstallWidget(installPath)
	if err != nil {
		return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
	}
	return nil
}

func BazaarIcons(keyword string) (icons []*bazaar.Icon) {
	icons = bazaar.Icons()
	icons = filterIcons(icons, keyword)
	for _, installed := range Conf.Appearance.Icons {
		for _, icon := range icons {
			if installed == icon.Name {
				icon.Installed = true
				if iconConf, err := bazaar.IconJSON(icon.Name); err == nil {
					icon.Outdated = 0 > semver.Compare("v"+iconConf.Version, "v"+icon.Version)
				}
			}
			icon.Current = icon.Name == Conf.Appearance.Icon
		}
	}
	return
}

func filterIcons(icons []*bazaar.Icon, keyword string) (ret []*bazaar.Icon) {
	keywords := getSearchKeywords(keyword)
	if 0 == len(keywords) {
		return icons
	}
	ret = []*bazaar.Icon{}
	for _, i := range icons {
		if matchPackage(keywords, i.Package) {
			ret = append(ret, i)
		}
	}
	return
}

func InstalledIcons(keyword string) (icons []*bazaar.Icon) {
	icons = bazaar.InstalledIcons()
	icons = filterIcons(icons, keyword)
	for _, icon := range icons {
		icon.Current = icon.Name == Conf.Appearance.Icon
	}
	return
}

func InstallBazaarIcon(repoURL, repoHash, iconName string) error {
	installPath := filepath.Join(util.IconsPath, iconName)
	err := bazaar.InstallIcon(repoURL, repoHash, installPath, Conf.System.ID)
	if err != nil {
		return errors.New(fmt.Sprintf(Conf.Language(46), iconName, err))
	}
	Conf.Appearance.Icon = iconName
	Conf.Save()
	InitAppearance()
	util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance)
	return nil
}

func UninstallBazaarIcon(iconName string) error {
	installPath := filepath.Join(util.IconsPath, iconName)
	err := bazaar.UninstallIcon(installPath)
	if err != nil {
		return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
	}

	InitAppearance()
	return nil
}

func BazaarThemes(keyword string) (ret []*bazaar.Theme) {
	ret = bazaar.Themes()
	ret = filterThemes(ret, keyword)
	installs := Conf.Appearance.DarkThemes
	installs = append(installs, Conf.Appearance.LightThemes...)
	for _, installed := range installs {
		for _, theme := range ret {
			if installed.Name == theme.Name {
				theme.Installed = true
				if themeConf, err := bazaar.ThemeJSON(theme.Name); err == nil {
					theme.Outdated = 0 > semver.Compare("v"+themeConf.Version, "v"+theme.Version)
				}
				theme.Current = theme.Name == Conf.Appearance.ThemeDark || theme.Name == Conf.Appearance.ThemeLight
			}
		}
	}
	return
}

func filterThemes(themes []*bazaar.Theme, keyword string) (ret []*bazaar.Theme) {
	keywords := getSearchKeywords(keyword)
	if 0 == len(keywords) {
		return themes
	}
	ret = []*bazaar.Theme{}
	for _, t := range themes {
		if matchPackage(keywords, t.Package) {
			ret = append(ret, t)
		}
	}
	return
}

func InstalledThemes(keyword string) (ret []*bazaar.Theme) {
	ret = bazaar.InstalledThemes()
	ret = filterThemes(ret, keyword)
	for _, theme := range ret {
		theme.Current = theme.Name == Conf.Appearance.ThemeDark || theme.Name == Conf.Appearance.ThemeLight
	}
	return
}

func InstallBazaarTheme(repoURL, repoHash, themeName string, mode int, update bool) error {
	closeThemeWatchers()

	installPath := filepath.Join(util.ThemesPath, themeName)
	err := bazaar.InstallTheme(repoURL, repoHash, installPath, Conf.System.ID)
	if err != nil {
		return errors.New(fmt.Sprintf(Conf.Language(46), themeName, err))
	}

	if !update {
		// 更新主题后不需要对该主题进行切换 https://github.com/siyuan-note/siyuan/issues/4966
		if 0 == mode {
			Conf.Appearance.ThemeLight = themeName
		} else {
			Conf.Appearance.ThemeDark = themeName
		}
		Conf.Appearance.Mode = mode
		Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(installPath, "theme.js"))
		Conf.Save()
	}

	InitAppearance()
	util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance)
	return nil
}

func UninstallBazaarTheme(themeName string) error {
	closeThemeWatchers()

	installPath := filepath.Join(util.ThemesPath, themeName)
	err := bazaar.UninstallTheme(installPath)
	if err != nil {
		return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
	}

	InitAppearance()
	return nil
}

func BazaarTemplates(keyword string) (templates []*bazaar.Template) {
	templates = bazaar.Templates()
	templates = filterTemplates(templates, keyword)
	for _, template := range templates {
		template.Installed = util.IsPathRegularDirOrSymlinkDir(filepath.Join(util.DataDir, "templates", template.Name))
		if template.Installed {
			if templateConf, err := bazaar.TemplateJSON(template.Name); err == nil && nil != templateConf {
				template.Outdated = 0 > semver.Compare("v"+templateConf.Version, "v"+template.Version)
			}
		} else {
			template.Outdated = false
		}
	}
	return
}

func filterTemplates(templates []*bazaar.Template, keyword string) (ret []*bazaar.Template) {
	keywords := getSearchKeywords(keyword)
	if 0 == len(keywords) {
		return templates
	}
	ret = []*bazaar.Template{}
	for _, t := range templates {
		if matchPackage(keywords, t.Package) {
			ret = append(ret, t)
		}
	}
	return
}

func InstalledTemplates(keyword string) (templates []*bazaar.Template) {
	templates = bazaar.InstalledTemplates()
	templates = filterTemplates(templates, keyword)
	return
}

func InstallBazaarTemplate(repoURL, repoHash, templateName string) error {
	installPath := filepath.Join(util.DataDir, "templates", templateName)
	err := bazaar.InstallTemplate(repoURL, repoHash, installPath, Conf.System.ID)
	if err != nil {
		return errors.New(fmt.Sprintf(Conf.Language(46), templateName, err))
	}
	return nil
}

func UninstallBazaarTemplate(templateName string) error {
	installPath := filepath.Join(util.DataDir, "templates", templateName)
	err := bazaar.UninstallTemplate(installPath)
	if err != nil {
		return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
	}
	return nil
}

func matchPackage(keywords []string, pkg *bazaar.Package) bool {
	if 1 > len(keywords) {
		return true
	}

	if nil == pkg {
		return false
	}

	for _, kw := range keywords {
		if !packageContainsKeyword(pkg, kw) {
			return false
		}
	}

	// 全部关键词匹配
	return true
}

func packageContainsKeyword(pkg *bazaar.Package, kw string) bool {
	if strings.Contains(strings.ToLower(path.Base(pkg.RepoURL)), kw) ||
		strings.Contains(strings.ToLower(pkg.Author), kw) {
		return true
	}
	for _, s := range pkg.DisplayName {
		if strings.Contains(strings.ToLower(s), kw) {
			return true
		}
	}
	for _, s := range pkg.Description {
		if strings.Contains(strings.ToLower(s), kw) {
			return true
		}
	}
	for _, s := range pkg.Keywords {
		if strings.Contains(strings.ToLower(s), kw) {
			return true
		}
	}
	return false
}

func getSearchKeywords(query string) (ret []string) {
	query = strings.TrimSpace(query)
	if "" == query {
		return
	}

	keywords := strings.Split(query, " ")
	for _, k := range keywords {
		if "" != k {
			ret = append(ret, strings.ToLower(k))
		}
	}
	return
}
