/*
*
*	Gosora Topic Store
*	Copyright Azareal 2017 - 2020
*
 */
package common

import (
	"database/sql"
	"errors"
	"strconv"
	"strings"

	qgen "github.com/Azareal/Gosora/query_gen"
)

// TODO: Add the watchdog goroutine
// TODO: Add some sort of update method
// ? - Should we add stick, lock, unstick, and unlock methods? These might be better on the Topics not the TopicStore
var Topics TopicStore
var ErrNoTitle = errors.New("This message is missing a title")
var ErrLongTitle = errors.New("The title is too long")
var ErrNoBody = errors.New("This message is missing a body")

type TopicStore interface {
	DirtyGet(id int) *Topic
	Get(id int) (*Topic, error)
	BypassGet(id int) (*Topic, error)
	BulkGetMap(ids []int) (list map[int]*Topic, err error)
	Exists(id int) bool
	Create(fid int, name, content string, uid int, ip string) (tid int, err error)
	AddLastTopic(t *Topic, fid int) error // unimplemented
	Reload(id int) error                  // Too much SQL logic to move into TopicCache
	// TODO: Implement these two methods
	//Replies(tid int) ([]*Reply, error)
	//RepliesRange(tid, lower, higher int) ([]*Reply, error)
	Count() int
	CountUser(uid int) int
	CountMegaUser(uid int) int
	CountBigUser(uid int) int

	ClearIPs() error
	LockMany(tids []int) error

	SetCache(cache TopicCache)
	GetCache() TopicCache
}

type DefaultTopicStore struct {
	cache TopicCache

	get           *sql.Stmt
	exists        *sql.Stmt
	count         *sql.Stmt
	countUser     *sql.Stmt
	countWordUser *sql.Stmt
	create        *sql.Stmt

	clearIPs *sql.Stmt
	lockTen *sql.Stmt
}

// NewDefaultTopicStore gives you a new instance of DefaultTopicStore
func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) {
	acc := qgen.NewAcc()
	if cache == nil {
		cache = NewNullTopicCache()
	}
	t := "topics"
	return &DefaultTopicStore{
		cache:         cache,
		get:           acc.Select(t).Columns("title,content,createdBy,createdAt,lastReplyBy,lastReplyAt,lastReplyID,is_closed,sticky,parentID,ip,views,postCount,likeCount,attachCount,poll,data").Where("tid=?").Stmt(),
		exists:        acc.Exists(t, "tid").Stmt(),
		count:         acc.Count(t).Stmt(),
		countUser:     acc.Count(t).Where("createdBy=?").Stmt(),
		countWordUser: acc.Count(t).Where("createdBy=? AND words>=?").Stmt(),
		create:        acc.Insert(t).Columns("parentID,title,content,parsed_content,createdAt,lastReplyAt,lastReplyBy,ip,words,createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(),

		clearIPs: acc.Update(t).Set("ip=''").Where("ip!=''").Stmt(),
		lockTen: acc.Update(t).Set("is_closed=1").Where("tid IN(" + inqbuild2(10) + ")").Stmt(),
	}, acc.FirstError()
}

func (s *DefaultTopicStore) DirtyGet(id int) *Topic {
	t, e := s.cache.Get(id)
	if e == nil {
		return t
	}
	t, e = s.BypassGet(id)
	if e == nil {
		_ = s.cache.Set(t)
		return t
	}
	return BlankTopic()
}

// TODO: Log weird cache errors?
func (s *DefaultTopicStore) Get(id int) (t *Topic, e error) {
	t, e = s.cache.Get(id)
	if e == nil {
		return t, nil
	}
	t, e = s.BypassGet(id)
	if e == nil {
		_ = s.cache.Set(t)
	}
	return t, e
}

// BypassGet will always bypass the cache and pull the topic directly from the database
func (s *DefaultTopicStore) BypassGet(id int) (*Topic, error) {
	t := &Topic{ID: id}
	e := s.get.QueryRow(id).Scan(&t.Title, &t.Content, &t.CreatedBy, &t.CreatedAt, &t.LastReplyBy, &t.LastReplyAt, &t.LastReplyID, &t.IsClosed, &t.Sticky, &t.ParentID, &t.IP, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data)
	if e == nil {
		t.Link = BuildTopicURL(NameToSlug(t.Title), id)
	}
	return t, e
}

/*func (s *DefaultTopicStore) GetByUser(uid int) (list map[int]*Topic, err error) {
	t := &Topic{ID: id}
	err := s.get.QueryRow(id).Scan(&t.Title, &t.Content, &t.CreatedBy, &t.CreatedAt, &t.LastReplyBy, &t.LastReplyAt, &t.LastReplyID, &t.IsClosed, &t.Sticky, &t.ParentID, &t.IP, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data)
	if err == nil {
		t.Link = BuildTopicURL(NameToSlug(t.Title), id)
	}
	return t, err
}*/

// TODO: Avoid duplicating much of this logic from user_store.go
func (s *DefaultTopicStore) BulkGetMap(ids []int) (list map[int]*Topic, e error) {
	idCount := len(ids)
	list = make(map[int]*Topic)
	if idCount == 0 {
		return list, nil
	}

	var stillHere []int
	sliceList := s.cache.BulkGet(ids)
	if len(sliceList) > 0 {
		for i, sliceItem := range sliceList {
			if sliceItem != nil {
				list[sliceItem.ID] = sliceItem
			} else {
				stillHere = append(stillHere, ids[i])
			}
		}
		ids = stillHere
	}

	// If every user is in the cache, then return immediately
	if len(ids) == 0 {
		return list, nil
	} else if len(ids) == 1 {
		t, e := s.Get(ids[0])
		if e != nil {
			return list, e
		}
		list[t.ID] = t
		return list, nil
	}

	idList, q := inqbuild(ids)
	rows, e := qgen.NewAcc().Select("topics").Columns("tid,title,content,createdBy,createdAt,lastReplyBy,lastReplyAt,lastReplyID,is_closed,sticky,parentID,ip,views,postCount,likeCount,attachCount,poll,data").Where("tid IN(" + q + ")").Query(idList...)
	if e != nil {
		return list, e
	}
	defer rows.Close()

	for rows.Next() {
		t := &Topic{}
		e := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.CreatedAt, &t.LastReplyBy, &t.LastReplyAt, &t.LastReplyID, &t.IsClosed, &t.Sticky, &t.ParentID, &t.IP, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data)
		if e != nil {
			return list, e
		}
		t.Link = BuildTopicURL(NameToSlug(t.Title), t.ID)
		_ = s.cache.Set(t)
		list[t.ID] = t
	}
	if e = rows.Err(); e != nil {
		return list, e
	}

	// Did we miss any topics?
	if idCount > len(list) {
		var sidList string
		for i, id := range ids {
			if _, ok := list[id]; !ok {
				if i == 0 {
					sidList += strconv.Itoa(id)
				} else {
					sidList += ","+strconv.Itoa(id)
				}
			}
		}
		if sidList != "" {
			e = errors.New("Unable to find topics with the following IDs: " + sidList)
		}
	}

	return list, e
}

func (s *DefaultTopicStore) Reload(id int) error {
	t, e := s.BypassGet(id)
	if e == nil {
		_ = s.cache.Set(t)
	} else {
		_ = s.cache.Remove(id)
	}
	TopicListThaw.Thaw()
	return e
}

func (s *DefaultTopicStore) Exists(id int) bool {
	return s.exists.QueryRow(id).Scan(&id) == nil
}

func (s *DefaultTopicStore) ClearIPs() error {
	_, e := s.clearIPs.Exec()
	return e
}

func (s *DefaultTopicStore) LockMany(tids []int) (e error) {
	tc, i := Topics.GetCache(), 0
	singles := func() error {
		for ; i < len(tids); i++ {
			_, e := topicStmts.lock.Exec(tids[i])
			if e != nil {
				return e
			}
		}
		return nil
	}

	if len(tids) < 10 {
		if e = singles(); e != nil {
			return e
		}
		if tc != nil {
			_ = tc.RemoveMany(tids)
		}
		TopicListThaw.Thaw()
		return nil
	}

	for ; (i + 10) < len(tids); i += 10 {
		_, e := s.lockTen.Exec(tids[i], tids[i+1], tids[i+2], tids[i+3], tids[i+4], tids[i+5], tids[i+6], tids[i+7], tids[i+8], tids[i+9])
		if e != nil {
			return e
		}
	}

	if e = singles(); e != nil {
		return e
	}
	if tc != nil {
		_ = tc.RemoveMany(tids)
	}
	TopicListThaw.Thaw()
	return nil
}

func (s *DefaultTopicStore) Create(fid int, name, content string, uid int, ip string) (tid int, err error) {
	if name == "" {
		return 0, ErrNoTitle
	}
	// ? This number might be a little screwy with Unicode, but it's the only consistent thing we have, as Unicode characters can be any number of bytes in theory?
	if len(name) > Config.MaxTopicTitleLength {
		return 0, ErrLongTitle
	}

	parsedContent := strings.TrimSpace(ParseMessage(content, fid, "forums", nil, nil))
	if parsedContent == "" {
		return 0, ErrNoBody
	}

	// TODO: Move this statement into the topic store
	if Config.DisablePostIP {
		ip = ""
	}
	res, err := s.create.Exec(fid, name, content, parsedContent, uid, ip, WordCount(content), uid)
	if err != nil {
		return 0, err
	}
	lastID, err := res.LastInsertId()
	if err != nil {
		return 0, err
	}
	tid = int(lastID)
	//TopicListThaw.Thaw() // redundant

	return tid, Forums.AddTopic(tid, uid, fid)
}

// ? - What is this? Do we need it? Should it be in the main store interface?
func (s *DefaultTopicStore) AddLastTopic(t *Topic, fid int) error {
	// Coming Soon...
	return nil
}

// Count returns the total number of topics on these forums
func (s *DefaultTopicStore) Count() (count int) {
	return Countf(s.count)
}
func (s *DefaultTopicStore) CountUser(uid int) (count int) {
	return Countf(s.countUser, uid)
}
func (s *DefaultTopicStore) CountMegaUser(uid int) (count int) {
	return Countf(s.countWordUser, uid, SettingBox.Load().(SettingMap)["megapost_min_words"].(int))
}
func (s *DefaultTopicStore) CountBigUser(uid int) (count int) {
	return Countf(s.countWordUser, uid, SettingBox.Load().(SettingMap)["bigpost_min_words"].(int))
}

func (s *DefaultTopicStore) SetCache(cache TopicCache) {
	s.cache = cache
}

// TODO: We're temporarily doing this so that you can do tcache != nil in getTopicUser. Refactor it.
func (s *DefaultTopicStore) GetCache() TopicCache {
	_, ok := s.cache.(*NullTopicCache)
	if ok {
		return nil
	}
	return s.cache
}