package arn

import (
	"fmt"
	"reflect"
	"sort"
	"strings"

	"github.com/aerogo/markdown"
	"github.com/aerogo/nano"
)

// PostID represents a post ID.
type PostID = ID

// Post is a comment related to any parent type in the database.
type Post struct {
	Tags       []string `json:"tags" editable:"true"`
	ParentID   ID       `json:"parentId" editable:"true"`
	ParentType string   `json:"parentType"`
	Edited     string   `json:"edited"`

	hasID
	hasText
	hasPosts
	hasCreator
	hasLikes

	html string
}

// Parent returns the object this post was posted in.
func (post *Post) Parent() PostParent {
	obj, _ := DB.Get(post.ParentType, post.ParentID)
	return obj.(PostParent)
}

// TopMostParent returns the first non-post object this post was posted in.
func (post *Post) TopMostParent() PostParent {
	topMostParent := post.Parent()

	for {
		if topMostParent.TypeName() != "Post" {
			return topMostParent
		}

		newParent := topMostParent.(*Post).Parent()

		if newParent == nil {
			return topMostParent
		}

		topMostParent = newParent
	}
}

// GetParentID returns the object ID of the parent.
func (post *Post) GetParentID() string {
	return post.ParentID
}

// GetParentType returns the object type of the parent.
func (post *Post) GetParentType() string {
	return post.ParentType
}

// SetParent sets a new parent.
func (post *Post) SetParent(newParent PostParent) {
	// Remove from old parent
	oldParent := post.Parent()
	oldParent.RemovePost(post.ID)
	oldParent.Save()

	// Update own fields
	post.ParentID = newParent.GetID()
	post.ParentType = reflect.TypeOf(newParent).Elem().Name()

	// Add to new parent
	newParent.AddPost(post.ID)
	newParent.Save()
}

// Link returns the relative URL of the post.
func (post *Post) Link() string {
	return "/post/" + post.ID
}

// TypeName returns the type name.
func (post *Post) TypeName() string {
	return "Post"
}

// Self returns the object itself.
func (post *Post) Self() Loggable {
	return post
}

// TitleByUser returns the preferred title for the given user.
func (post *Post) TitleByUser(user *User) string {
	return post.Creator().Nick + "'s comment"
}

// HTML returns the HTML representation of the post.
func (post *Post) HTML() string {
	if post.html != "" {
		return post.html
	}

	post.html = markdown.Render(post.Text)
	return post.html
}

// String implements the default string serialization.
func (post *Post) String() string {
	const maxLen = 170

	if len(post.Text) > maxLen {
		return post.Text[:maxLen-3] + "..."
	}

	return post.Text
}

// OnLike is called when the post receives a like.
func (post *Post) OnLike(likedBy *User) {
	if !post.Creator().Settings().Notification.ForumLikes {
		return
	}

	go func() {
		message := ""
		notifyUser := post.Creator()

		if post.ParentType == "User" {
			if post.ParentID == notifyUser.ID {
				// Somebody liked your post on your own profile
				message = fmt.Sprintf(`%s liked your profile post.`, likedBy.Nick)
			} else {
				// Somebody liked your post on someone else's profile
				message = fmt.Sprintf(`%s liked your post on %s's profile.`, likedBy.Nick, post.Parent().TitleByUser(notifyUser))
			}
		} else {
			message = fmt.Sprintf(`%s liked your post in the %s "%s".`, likedBy.Nick, strings.ToLower(post.ParentType), post.Parent().TitleByUser(notifyUser))
		}

		notifyUser.SendNotification(&PushNotification{
			Title:   likedBy.Nick + " liked your post",
			Message: message,
			Icon:    "https:" + likedBy.AvatarLink("large"),
			Link:    "https://notify.moe" + likedBy.Link(),
			Type:    NotificationTypeLike,
		})
	}()
}

// GetPost ...
func GetPost(id PostID) (*Post, error) {
	obj, err := DB.Get("Post", id)

	if err != nil {
		return nil, err
	}

	return obj.(*Post), nil
}

// StreamPosts returns a stream of all posts.
func StreamPosts() <-chan *Post {
	channel := make(chan *Post, nano.ChannelBufferSize)

	go func() {
		for obj := range DB.All("Post") {
			channel <- obj.(*Post)
		}

		close(channel)
	}()

	return channel
}

// AllPosts returns a slice of all posts.
func AllPosts() []*Post {
	all := make([]*Post, 0, DB.Collection("Post").Count())

	for obj := range StreamPosts() {
		all = append(all, obj)
	}

	return all
}

// SortPostsLatestFirst sorts the slice of posts.
func SortPostsLatestFirst(posts []*Post) {
	sort.Slice(posts, func(i, j int) bool {
		return posts[i].Created > posts[j].Created
	})
}

// SortPostsLatestLast sorts the slice of posts.
func SortPostsLatestLast(posts []*Post) {
	sort.Slice(posts, func(i, j int) bool {
		return posts[i].Created < posts[j].Created
	})
}

// FilterPostsWithUniqueThreads removes posts with the same thread until we have enough posts.
func FilterPostsWithUniqueThreads(posts []*Post, limit int) []*Post {
	filtered := []*Post{}
	threadsProcessed := map[string]bool{}

	for _, post := range posts {
		if len(filtered) >= limit {
			return filtered
		}

		_, found := threadsProcessed[post.ParentID]

		if found {
			continue
		}

		threadsProcessed[post.ParentID] = true
		filtered = append(filtered, post)
	}

	return filtered
}

// GetPostsByUser ...
func GetPostsByUser(user *User) ([]*Post, error) {
	var posts []*Post

	for post := range StreamPosts() {
		if post.CreatedBy == user.ID {
			posts = append(posts, post)
		}
	}

	return posts, nil
}

// FilterPosts filters all forum posts by a custom function.
func FilterPosts(filter func(*Post) bool) ([]*Post, error) {
	var filtered []*Post

	for post := range StreamPosts() {
		if filter(post) {
			filtered = append(filtered, post)
		}
	}

	return filtered, nil
}