package boards

import (
	"std"
	"strconv"

	"gno.land/p/avl"
)

//----------------------------------------
// Post

// NOTE: a PostID is relative to the board.
type PostID uint64

func (pid PostID) String() string {
	return strconv.Itoa(int(pid))
}

// A Post is a "thread" or a "reply" depending on context.
// A thread is a Post of a Board that holds other replies.
type Post struct {
	board       *Board
	id          PostID
	creator     std.Address
	title       string // optional
	body        string
	replies     *avl.MutTree // Post.id -> *Post
	repliesAll  *avl.MutTree // Post.id -> *Post (all replies, for top-level posts)
	reposts     *avl.MutTree // Board.id -> Post.id
	threadID    PostID       // original Post.id
	parentID    PostID       // parent Post.id (if reply or repost)
	repostBoard BoardID      // original Board.id (if repost)
	createdAt   std.Time
	updatedAt   std.Time
}

func newPost(board *Board, id PostID, creator std.Address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post {
	return &Post{
		board:       board,
		id:          id,
		creator:     creator,
		title:       title,
		body:        body,
		replies:     avl.NewMutTree(),
		repliesAll:  avl.NewMutTree(),
		reposts:     avl.NewMutTree(),
		threadID:    threadID,
		parentID:    parentID,
		repostBoard: repostBoard,
		createdAt:   std.GetTimestamp(),
	}
}

func (post *Post) IsThread() bool {
	return post.parentID == 0
}

func (post *Post) GetPostID() PostID {
	return post.id
}

func (post *Post) AddReply(creator std.Address, body string) *Post {
	board := post.board
	pid := board.incGetPostID()
	pidkey := postIDKey(pid)
	reply := newPost(board, pid, creator, "", body, post.threadID, post.id, 0)
	post.replies.Set(pidkey, reply)
	if post.threadID == post.id {
		post.repliesAll.Set(pidkey, reply)
	} else {
		thread := board.GetThread(post.threadID)
		thread.repliesAll.Set(pidkey, reply)
	}
	return reply
}

func (post *Post) Update(title string, body string) {
	post.title = title
	post.body = body
	post.updatedAt = std.GetTimestamp()
}

func (thread *Post) GetReply(pid PostID) *Post {
	pidkey := postIDKey(pid)
	replyI, ok := thread.repliesAll.Get(pidkey)
	if !ok {
		return nil
	} else {
		return replyI.(*Post)
	}
}

func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post {
	if !post.IsThread() {
		panic("cannot repost non-thread post")
	}
	pid := dst.incGetPostID()
	pidkey := postIDKey(pid)
	repost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id)
	dst.threads.Set(pidkey, repost)
	if !dst.IsPrivate() {
		bidkey := boardIDKey(dst.id)
		post.reposts.Set(bidkey, pid)
	}
	return repost
}

func (thread *Post) DeletePost(pid PostID) {
	if thread.id == pid {
		panic("should not happen")
	}
	pidkey := postIDKey(pid)
	postI, removed := thread.repliesAll.Remove(pidkey)
	if !removed {
		panic("post not found in thread")
	}
	post := postI.(*Post)
	if post.parentID != thread.id {
		parent := thread.GetReply(post.parentID)
		parent.replies.Remove(pidkey)
	} else {
		thread.replies.Remove(pidkey)
	}
}

func (post *Post) HasPermission(addr std.Address, perm Permission) bool {
	if post.creator == addr {
		switch perm {
		case EditPermission:
			return true
		case DeletePermission:
			return true
		default:
			return false
		}
	}
	// post notes inherit permissions of the board.
	return post.board.HasPermission(addr, perm)
}

func (post *Post) GetSummary() string {
	return summaryOf(post.body, 80)
}

func (post *Post) GetURL() string {
	if post.IsThread() {
		return post.board.GetURLFromThreadAndReplyID(
			post.id, 0)
	} else {
		return post.board.GetURLFromThreadAndReplyID(
			post.threadID, post.id)
	}
}

func (post *Post) GetReplyFormURL() string {
	return "/r/boards?help&__func=CreateReply" +
		"&bid=" + post.board.id.String() +
		"&threadid=" + post.threadID.String() +
		"&postid=" + post.id.String() +
		"&body.type=textarea"
}

func (post *Post) GetDeleteFormURL() string {
	return "/r/boards?help&__func=DeletePost" +
		"&bid=" + post.board.id.String() +
		"&threadid=" + post.threadID.String() +
		"&postid=" + post.id.String()
}

func (post *Post) RenderSummary() string {
	str := ""
	if post.title != "" {
		str += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n"
		str += "\n"
	}
	str += post.GetSummary() + "\n"
	str += "\\- " + displayAddressMD(post.creator) + ","
	str += " [" + std.FormatTimestamp(post.createdAt, "2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")"
	str += " \\[[x](" + post.GetDeleteFormURL() + ")]"
	str += " (" + strconv.Itoa(post.replies.Size()) + " replies)" + "\n"
	return str
}

func (post *Post) RenderPost(indent string, levels int) string {
	if post == nil {
		return "nil post"
	}
	str := ""
	if post.title != "" {
		str += indent + "# " + post.title + "\n"
		str += indent + "\n"
	}
	str += indentBody(indent, post.body) + "\n" // TODO: indent body lines.
	str += indent + "\\- " + displayAddressMD(post.creator) + ", "
	str += "[" + std.FormatTimestamp(post.createdAt, "2006-01-02 3:04pm (MST)") + "](" + post.GetURL() + ")"
	str += " \\[[reply](" + post.GetReplyFormURL() + ")]"
	str += " \\[[x](" + post.GetDeleteFormURL() + ")]\n"
	if levels > 0 {
		if post.replies.Size() > 0 {
			post.replies.Iterate("", "", func(n *avl.Tree) bool {
				str += indent + "\n"
				str += n.Value().(*Post).RenderPost(indent+"> ", levels-1)
				return false
			})
		}
	} else {
		if post.replies.Size() > 0 {
			str += indent + "\n"
			str += indent + "_[see all " + strconv.Itoa(post.replies.Size()) + " replies](" + post.GetURL() + ")_\n"
		}
	}
	return str
}

// render reply and link to context thread
func (post *Post) RenderInner() string {
	if post.IsThread() {
		panic("unexpected thread")
	}
	threadID := post.threadID
	// replyID := post.id
	parentID := post.parentID
	str := ""
	str += "_[see thread](" + post.board.GetURLFromThreadAndReplyID(
		threadID, 0) + ")_\n\n"
	thread := post.board.GetThread(post.threadID)
	var parent *Post
	if thread.id == parentID {
		parent = thread
	} else {
		parent = thread.GetReply(parentID)
	}
	str += parent.RenderPost("", 0)
	str += "\n"
	str += post.RenderPost("> ", 5)
	return str
}