package ast

import (
	"fmt"

	"github.com/robertkrimen/otto/file"
)

// CommentPosition determines where the comment is in a given context.
type CommentPosition int

// Available comment positions.
const (
	_ CommentPosition = iota
	// LEADING is before the pertinent expression.
	LEADING
	// TRAILING is after the pertinent expression.
	TRAILING
	// KEY is before a key in an object.
	KEY
	// COLON is after a colon in a field declaration.
	COLON
	// FINAL is the final comments in a block, not belonging to a specific expression or the comment after a trailing , in an array or object literal.
	FINAL
	// IF is after an if keyword.
	IF
	// WHILE is after a while keyword.
	WHILE
	// DO is after do keyword.
	DO
	// FOR is after a for keyword.
	FOR
	// WITH is after a with keyword.
	WITH
	// TBD is unknown.
	TBD
)

// Comment contains the data of the comment.
type Comment struct {
	Text     string
	Begin    file.Idx
	Position CommentPosition
}

// NewComment creates a new comment.
func NewComment(text string, idx file.Idx) *Comment {
	comment := &Comment{
		Begin:    idx,
		Text:     text,
		Position: TBD,
	}

	return comment
}

// String returns a stringified version of the position.
func (cp CommentPosition) String() string {
	switch cp {
	case LEADING:
		return "Leading"
	case TRAILING:
		return "Trailing"
	case KEY:
		return "Key"
	case COLON:
		return "Colon"
	case FINAL:
		return "Final"
	case IF:
		return "If"
	case WHILE:
		return "While"
	case DO:
		return "Do"
	case FOR:
		return "For"
	case WITH:
		return "With"
	default:
		return "???"
	}
}

// String returns a stringified version of the comment.
func (c Comment) String() string {
	return fmt.Sprintf("Comment: %v", c.Text)
}

// Comments defines the current view of comments from the parser.
type Comments struct {
	// CommentMap is a reference to the parser comment map
	CommentMap CommentMap
	// Comments lists the comments scanned, not linked to a node yet
	Comments []*Comment
	// Current is node for which comments are linked to
	Current Expression

	// future lists the comments after a line break during a sequence of comments
	future []*Comment
	// wasLineBreak determines if a line break occurred while scanning for comments
	wasLineBreak bool
	// primary determines whether or not processing a primary expression
	primary bool
	// afterBlock determines whether or not being after a block statement
	afterBlock bool
}

// NewComments returns a new Comments.
func NewComments() *Comments {
	comments := &Comments{
		CommentMap: CommentMap{},
	}

	return comments
}

func (c *Comments) String() string {
	return fmt.Sprintf("NODE: %v, Comments: %v, Future: %v(LINEBREAK:%v)", c.Current, len(c.Comments), len(c.future), c.wasLineBreak)
}

// FetchAll returns all the currently scanned comments,
// including those from the next line.
func (c *Comments) FetchAll() []*Comment {
	defer func() {
		c.Comments = nil
		c.future = nil
	}()

	return append(c.Comments, c.future...)
}

// Fetch returns all the currently scanned comments.
func (c *Comments) Fetch() []*Comment {
	defer func() {
		c.Comments = nil
	}()

	return c.Comments
}

// ResetLineBreak marks the beginning of a new statement.
func (c *Comments) ResetLineBreak() {
	c.wasLineBreak = false
}

// MarkPrimary will mark the context as processing a primary expression.
func (c *Comments) MarkPrimary() {
	c.primary = true
	c.wasLineBreak = false
}

// AfterBlock will mark the context as being after a block.
func (c *Comments) AfterBlock() {
	c.afterBlock = true
}

// AddComment adds a comment to the view.
// Depending on the context, comments are added normally or as post line break.
func (c *Comments) AddComment(comment *Comment) {
	if c.primary {
		if !c.wasLineBreak {
			c.Comments = append(c.Comments, comment)
		} else {
			c.future = append(c.future, comment)
		}
	} else {
		if !c.wasLineBreak || (c.Current == nil && !c.afterBlock) {
			c.Comments = append(c.Comments, comment)
		} else {
			c.future = append(c.future, comment)
		}
	}
}

// MarkComments will mark the found comments as the given position.
func (c *Comments) MarkComments(position CommentPosition) {
	for _, comment := range c.Comments {
		if comment.Position == TBD {
			comment.Position = position
		}
	}
	for _, c := range c.future {
		if c.Position == TBD {
			c.Position = position
		}
	}
}

// Unset the current node and apply the comments to the current expression.
// Resets context variables.
func (c *Comments) Unset() {
	if c.Current != nil {
		c.applyComments(c.Current, c.Current, TRAILING)
		c.Current = nil
	}
	c.wasLineBreak = false
	c.primary = false
	c.afterBlock = false
}

// SetExpression sets the current expression.
// It is applied the found comments, unless the previous expression has not been unset.
// It is skipped if the node is already set or if it is a part of the previous node.
func (c *Comments) SetExpression(node Expression) {
	// Skipping same node
	if c.Current == node {
		return
	}
	if c.Current != nil && c.Current.Idx1() == node.Idx1() {
		c.Current = node
		return
	}
	previous := c.Current
	c.Current = node

	// Apply the found comments and futures to the node and the previous.
	c.applyComments(node, previous, TRAILING)
}

// PostProcessNode applies all found comments to the given node.
func (c *Comments) PostProcessNode(node Node) {
	c.applyComments(node, nil, TRAILING)
}

// applyComments applies both the comments and the future comments to the given node and the previous one,
// based on the context.
func (c *Comments) applyComments(node, previous Node, position CommentPosition) {
	if previous != nil {
		c.CommentMap.AddComments(previous, c.Comments, position)
		c.Comments = nil
	} else {
		c.CommentMap.AddComments(node, c.Comments, position)
		c.Comments = nil
	}
	// Only apply the future comments to the node if the previous is set.
	// This is for detecting end of line comments and which node comments on the following lines belongs to
	if previous != nil {
		c.CommentMap.AddComments(node, c.future, position)
		c.future = nil
	}
}

// AtLineBreak will mark a line break.
func (c *Comments) AtLineBreak() {
	c.wasLineBreak = true
}

// CommentMap is the data structure where all found comments are stored.
type CommentMap map[Node][]*Comment

// AddComment adds a single comment to the map.
func (cm CommentMap) AddComment(node Node, comment *Comment) {
	list := cm[node]
	list = append(list, comment)

	cm[node] = list
}

// AddComments adds a slice of comments, given a node and an updated position.
func (cm CommentMap) AddComments(node Node, comments []*Comment, position CommentPosition) {
	for _, comment := range comments {
		if comment.Position == TBD {
			comment.Position = position
		}
		cm.AddComment(node, comment)
	}
}

// Size returns the size of the map.
func (cm CommentMap) Size() int {
	size := 0
	for _, comments := range cm {
		size += len(comments)
	}

	return size
}

// MoveComments moves comments with a given position from a node to another.
func (cm CommentMap) MoveComments(from, to Node, position CommentPosition) {
	for i, c := range cm[from] {
		if c.Position == position {
			cm.AddComment(to, c)

			// Remove the comment from the "from" slice
			cm[from][i] = cm[from][len(cm[from])-1]
			cm[from][len(cm[from])-1] = nil
			cm[from] = cm[from][:len(cm[from])-1]
		}
	}
}
