package js

import (
	"github.com/tdewolff/parse/v2/js"
)

func optimizeStmt(i js.IStmt) js.IStmt {
	// convert if/else into expression statement, and optimize blocks
	if ifStmt, ok := i.(*js.IfStmt); ok {
		if truthy, ok := isTruthy(ifStmt.Cond); ok && truthy {
			if hasSideEffects(ifStmt.Cond) {
				ifStmt.Else = nil
				return i // TODO: remove if and return StmtList(Cond, Body)
			}
			return optimizeStmt(ifStmt.Body)
		} else if ok {
			// falsy
			if isEmptyStmt(ifStmt.Else) {
				if hasSideEffects(ifStmt.Cond) {
					return &js.ExprStmt{Value: ifStmt.Cond}
				}
				return &js.EmptyStmt{}
			} else if hasSideEffects(ifStmt.Cond) {
				if unaryExpr, ok := ifStmt.Cond.(*js.UnaryExpr); ok && unaryExpr.Op == js.NotToken {
					ifStmt.Cond = unaryExpr.X
				} else {
					ifStmt.Cond = &js.UnaryExpr{js.NotToken, ifStmt.Cond}
				}
				ifStmt.Body, ifStmt.Else = ifStmt.Else, nil
				return i // TODO: remove if and return StmtList(Cond, Body)
			}
			return optimizeStmt(ifStmt.Else)
		}

		ifStmt.Body = optimizeStmt(ifStmt.Body)
		if ifStmt.Else != nil {
			ifStmt.Else = optimizeStmt(ifStmt.Else)
		}
		hasIf := !isEmptyStmt(ifStmt.Body)
		hasElse := !isEmptyStmt(ifStmt.Else)
		if unaryExpr, ok := ifStmt.Cond.(*js.UnaryExpr); ok && unaryExpr.Op == js.NotToken && hasElse {
			ifStmt.Cond = unaryExpr.X
			ifStmt.Body, ifStmt.Else = ifStmt.Else, ifStmt.Body
			hasIf, hasElse = hasElse, hasIf
		}
		if !hasIf && !hasElse {
			if hasSideEffects(ifStmt.Cond) {
				return &js.ExprStmt{Value: ifStmt.Cond}
			}
			return &js.EmptyStmt{}
		} else if hasIf && !hasElse {
			if X, isExprBody := ifStmt.Body.(*js.ExprStmt); isExprBody {
				if unaryExpr, ok := ifStmt.Cond.(*js.UnaryExpr); ok && unaryExpr.Op == js.NotToken {
					left := groupExpr(unaryExpr.X, binaryLeftPrecMap[js.OrToken])
					right := groupExpr(X.Value, binaryRightPrecMap[js.OrToken])
					return &js.ExprStmt{&js.BinaryExpr{js.OrToken, left, right}}
				}
				left := groupExpr(ifStmt.Cond, binaryLeftPrecMap[js.AndToken])
				right := groupExpr(X.Value, binaryRightPrecMap[js.AndToken])
				return &js.ExprStmt{&js.BinaryExpr{js.AndToken, left, right}}
			} else if X, isIfStmt := ifStmt.Body.(*js.IfStmt); isIfStmt && isEmptyStmt(X.Else) {
				left := groupExpr(ifStmt.Cond, binaryLeftPrecMap[js.AndToken])
				right := groupExpr(X.Cond, binaryRightPrecMap[js.AndToken])
				ifStmt.Cond = &js.BinaryExpr{js.AndToken, left, right}
				ifStmt.Body = X.Body
				return ifStmt
			}
		} else if !hasIf && hasElse {
			if X, isExprElse := ifStmt.Else.(*js.ExprStmt); isExprElse {
				left := groupExpr(ifStmt.Cond, binaryLeftPrecMap[js.OrToken])
				right := groupExpr(X.Value, binaryRightPrecMap[js.OrToken])
				return &js.ExprStmt{&js.BinaryExpr{js.OrToken, left, right}}
			}
		} else if hasIf && hasElse {
			XExpr, isExprBody := ifStmt.Body.(*js.ExprStmt)
			YExpr, isExprElse := ifStmt.Else.(*js.ExprStmt)
			if isExprBody && isExprElse {
				return &js.ExprStmt{condExpr(ifStmt.Cond, XExpr.Value, YExpr.Value)}
			}
			XReturn, isReturnBody := ifStmt.Body.(*js.ReturnStmt)
			YReturn, isReturnElse := ifStmt.Else.(*js.ReturnStmt)
			if isReturnBody && isReturnElse {
				if XReturn.Value == nil && YReturn.Value == nil {
					return &js.ReturnStmt{commaExpr(ifStmt.Cond, &js.UnaryExpr{
						Op: js.VoidToken,
						X:  &js.LiteralExpr{js.NumericToken, zeroBytes},
					})}
				} else if XReturn.Value != nil && YReturn.Value != nil {
					return &js.ReturnStmt{condExpr(ifStmt.Cond, XReturn.Value, YReturn.Value)}
				}
				return ifStmt
			}
			XThrow, isThrowBody := ifStmt.Body.(*js.ThrowStmt)
			YThrow, isThrowElse := ifStmt.Else.(*js.ThrowStmt)
			if isThrowBody && isThrowElse {
				return &js.ThrowStmt{condExpr(ifStmt.Cond, XThrow.Value, YThrow.Value)}
			}
		}
	} else if decl, ok := i.(*js.VarDecl); ok {
		// TODO: remove function name in var name=function name(){}
		//for _, item := range decl.List {
		//	if v, ok := item.Binding.(*js.Var); ok && item.Default != nil {
		//		if fun, ok := item.Default.(*js.FuncDecl); ok && fun.Name != nil && bytes.Equal(v.Data, fun.Name.Data) {
		//			scope := fun.Body.Scope
		//			for i, vorig := range scope.Declared {
		//				if fun.Name == vorig {
		//					scope.Declared = append(scope.Declared[:i], scope.Declared[i+1:]...)
		//				}
		//			}
		//			scope.AddUndeclared(v)
		//			v.Uses += fun.Name.Uses - 1
		//			fun.Name.Link = v
		//			fun.Name = nil
		//		}
		//	}
		//}

		if decl.TokenType == js.ErrorToken {
			// convert hoisted var declaration to expression or empty (if there are no defines) statement
			for _, item := range decl.List {
				if item.Default != nil {
					return &js.ExprStmt{Value: decl}
				}
			}
			return &js.EmptyStmt{}
		}
		// TODO: remove unused declarations
		//for i := 0; i < len(decl.List); i++ {
		//	if v, ok := decl.List[i].Binding.(*js.Var); ok && v.Uses < 2 {
		//		decl.List = append(decl.List[:i], decl.List[i+1:]...)
		//		i--
		//	}
		//}
		//if len(decl.List) == 0 {
		//	return &js.EmptyStmt{}
		//}
		return decl
	} else if blockStmt, ok := i.(*js.BlockStmt); ok {
		// merge body and remove braces if it is not a lexical declaration
		blockStmt.List = optimizeStmtList(blockStmt.List, defaultBlock)
		if len(blockStmt.List) == 1 {
			if _, ok := blockStmt.List[0].(*js.ClassDecl); ok {
				return &js.EmptyStmt{}
			} else if varDecl, ok := blockStmt.List[0].(*js.VarDecl); ok && varDecl.TokenType != js.VarToken {
				// remove let or const declaration in otherwise empty scope, but keep assignments
				exprs := []js.IExpr{}
				for _, item := range varDecl.List {
					if bindingUsed(item.Binding) {
						return blockStmt
					} else if item.Default != nil && hasSideEffects(item.Default) {
						exprs = append(exprs, item.Default)
					}
				}
				if len(exprs) == 0 {
					return &js.EmptyStmt{}
				} else if len(exprs) == 1 {
					return &js.ExprStmt{exprs[0]}
				}
				return &js.ExprStmt{&js.CommaExpr{exprs}}
			}
			return optimizeStmt(blockStmt.List[0])
		} else if len(blockStmt.List) == 0 {
			return &js.EmptyStmt{}
		}
		return blockStmt
	}
	return i
}

func optimizeStmtList(list []js.IStmt, blockType blockType) []js.IStmt {
	// merge expression statements as well as if/else statements followed by flow control statements
	if len(list) == 0 {
		return list
	}
	j := 0                           // write index
	for i := 0; i < len(list); i++ { // read index
		if ifStmt, ok := list[i].(*js.IfStmt); ok && !isEmptyStmt(ifStmt.Else) {
			// if(a)return b;else c  =>  if(a)b; c
			if unary, ok := ifStmt.Cond.(*js.UnaryExpr); ok && unary.Op == js.NotToken && isFlowStmt(lastStmt(ifStmt.Else)) {
				ifStmt.Cond = unary.X
				ifStmt.Body, ifStmt.Else = ifStmt.Else, ifStmt.Body
			}
			if isFlowStmt(lastStmt(ifStmt.Body)) {
				// if body ends in flow statement (return, throw, break, continue), we can remove the else statement and put its body in the current scope
				if blockStmt, ok := ifStmt.Else.(*js.BlockStmt); ok {
					blockStmt.Scope.Unscope()
					list = append(list[:i+1], append(blockStmt.List, list[i+1:]...)...)
				} else {
					list = append(list[:i+1], append([]js.IStmt{ifStmt.Else}, list[i+1:]...)...)
				}
				ifStmt.Else = nil
			}
		}

		list[i] = optimizeStmt(list[i])

		if _, ok := list[i].(*js.EmptyStmt); ok {
			k := i + 1
			for ; k < len(list); k++ {
				if _, ok := list[k].(*js.EmptyStmt); !ok {
					break
				}
			}
			list = append(list[:i], list[k:]...)
			i--
			continue
		}

		if 0 < i {
			// merge expression statements with expression, return, and throw statements
			if left, ok := list[i-1].(*js.ExprStmt); ok {
				if right, ok := list[i].(*js.ExprStmt); ok {
					right.Value = commaExpr(left.Value, right.Value)
					j--
				} else if returnStmt, ok := list[i].(*js.ReturnStmt); ok && returnStmt.Value != nil {
					returnStmt.Value = commaExpr(left.Value, returnStmt.Value)
					j--
				} else if throwStmt, ok := list[i].(*js.ThrowStmt); ok {
					throwStmt.Value = commaExpr(left.Value, throwStmt.Value)
					j--
				} else if forStmt, ok := list[i].(*js.ForStmt); ok {
					// TODO: only merge lhs expression that don't have 'in' or 'of' keywords (slow to check?)
					if forStmt.Init == nil {
						forStmt.Init = left.Value
						j--
					} else if decl, ok := forStmt.Init.(*js.VarDecl); ok && len(decl.List) == 0 {
						forStmt.Init = left.Value
						j--
					} else if ok && (decl.TokenType == js.VarToken || decl.TokenType == js.ErrorToken) {
						// this is the second VarDecl, so we are hoisting var declarations, which means the forInit variables are already in 'left'
						if merge := mergeVarDeclExprStmt(decl, left, true); merge {
							j--
						}
					}
				} else if whileStmt, ok := list[i].(*js.WhileStmt); ok {
					// TODO: only merge lhs expression that don't have 'in' or 'of' keywords (slow to check?)
					var body *js.BlockStmt
					if blockStmt, ok := whileStmt.Body.(*js.BlockStmt); ok {
						body = blockStmt
					} else {
						body = &js.BlockStmt{}
						body.List = []js.IStmt{whileStmt.Body}
					}
					list[i] = &js.ForStmt{Init: left.Value, Cond: whileStmt.Cond, Post: nil, Body: body}
					j--
				} else if switchStmt, ok := list[i].(*js.SwitchStmt); ok {
					switchStmt.Init = commaExpr(left.Value, switchStmt.Init)
					j--
				} else if withStmt, ok := list[i].(*js.WithStmt); ok {
					withStmt.Cond = commaExpr(left.Value, withStmt.Cond)
					j--
				} else if ifStmt, ok := list[i].(*js.IfStmt); ok {
					ifStmt.Cond = commaExpr(left.Value, ifStmt.Cond)
					j--
				} else if varDecl, ok := list[i].(*js.VarDecl); ok && varDecl.TokenType == js.VarToken {
					if merge := mergeVarDeclExprStmt(varDecl, left, true); merge {
						j--
					}
				}
			} else if left, ok := list[i-1].(*js.VarDecl); ok {
				if right, ok := list[i].(*js.VarDecl); ok && left.TokenType == right.TokenType {
					// merge const and let declarations, or non-hoisted var declarations
					right.List = append(left.List, right.List...)
					j--

					// remove from vardecls list of scope
					scope := left.Scope.Func
					for i, decl := range scope.VarDecls {
						if left == decl {
							scope.VarDecls = append(scope.VarDecls[:i], scope.VarDecls[i+1:]...)
							break
						}
					}
				} else if left.TokenType == js.VarToken {
					if exprStmt, ok := list[i].(*js.ExprStmt); ok {
						// pull in assignments to variables into the declaration, e.g. var a;a=5  =>  var a=5
						if merge := mergeVarDeclExprStmt(left, exprStmt, false); merge {
							list[i] = list[i-1]
							j--
						}
					} else if forStmt, ok := list[i].(*js.ForStmt); ok {
						// TODO: only merge lhs expression that don't have 'in' or 'of' keywords (slow to check?)
						if forStmt.Init == nil {
							forStmt.Init = left
							j--
						} else if decl, ok := forStmt.Init.(*js.VarDecl); ok && decl.TokenType == js.ErrorToken && !hasDefines(decl) {
							forStmt.Init = left
							j--
						} else if ok && (decl.TokenType == js.VarToken || decl.TokenType == js.ErrorToken) {
							// this is the second VarDecl, so we are hoisting var declarations, which means the forInit variables are already in 'left'
							mergeVarDecls(left, decl, false)
							decl.TokenType = js.VarToken
							forStmt.Init = left
							j--
						}
					} else if whileStmt, ok := list[i].(*js.WhileStmt); ok {
						// TODO: only merge lhs expression that don't have 'in' or 'of' keywords (slow to check?)
						var body *js.BlockStmt
						if blockStmt, ok := whileStmt.Body.(*js.BlockStmt); ok {
							body = blockStmt
						} else {
							body = &js.BlockStmt{}
							body.List = []js.IStmt{whileStmt.Body}
						}
						list[i] = &js.ForStmt{Init: left, Cond: whileStmt.Cond, Post: nil, Body: body}
						j--
					}
				}
			}
		}
		list[j] = list[i]

		// merge if/else with return/throw when followed by return/throw
	MergeIfReturnThrow:
		if 0 < j {
			// separate from expression merging in case of:  if(a)return b;b=c;return d
			if ifStmt, ok := list[j-1].(*js.IfStmt); ok && isEmptyStmt(ifStmt.Body) != isEmptyStmt(ifStmt.Else) {
				// either the if body is empty or the else body is empty. In case where both bodies have return/throw, we already rewrote that if statement to an return/throw statement
				if returnStmt, ok := list[j].(*js.ReturnStmt); ok {
					if returnStmt.Value == nil {
						if left, ok := ifStmt.Body.(*js.ReturnStmt); ok && left.Value == nil {
							list[j-1] = &js.ExprStmt{Value: ifStmt.Cond}
						} else if left, ok := ifStmt.Else.(*js.ReturnStmt); ok && left.Value == nil {
							list[j-1] = &js.ExprStmt{Value: ifStmt.Cond}
						}
					} else {
						if left, ok := ifStmt.Body.(*js.ReturnStmt); ok && left.Value != nil {
							returnStmt.Value = condExpr(ifStmt.Cond, left.Value, returnStmt.Value)
							list[j-1] = returnStmt
							j--
							goto MergeIfReturnThrow
						} else if left, ok := ifStmt.Else.(*js.ReturnStmt); ok && left.Value != nil {
							returnStmt.Value = condExpr(ifStmt.Cond, returnStmt.Value, left.Value)
							list[j-1] = returnStmt
							j--
							goto MergeIfReturnThrow
						}
					}
				} else if throwStmt, ok := list[j].(*js.ThrowStmt); ok {
					if left, ok := ifStmt.Body.(*js.ThrowStmt); ok {
						throwStmt.Value = condExpr(ifStmt.Cond, left.Value, throwStmt.Value)
						list[j-1] = throwStmt
						j--
						goto MergeIfReturnThrow
					} else if left, ok := ifStmt.Else.(*js.ThrowStmt); ok {
						throwStmt.Value = condExpr(ifStmt.Cond, throwStmt.Value, left.Value)
						list[j-1] = throwStmt
						j--
						goto MergeIfReturnThrow
					}
				}
			}
		}
		j++
	}

	// remove superfluous return or continue
	if 0 < j {
		if blockType == functionBlock {
			if returnStmt, ok := list[j-1].(*js.ReturnStmt); ok {
				if returnStmt.Value == nil || isUndefined(returnStmt.Value) {
					j--
				} else if commaExpr, ok := returnStmt.Value.(*js.CommaExpr); ok && isUndefined(commaExpr.List[len(commaExpr.List)-1]) {
					// rewrite function f(){return a,void 0} => function f(){a}
					if len(commaExpr.List) == 2 {
						list[j-1] = &js.ExprStmt{Value: commaExpr.List[0]}
					} else {
						commaExpr.List = commaExpr.List[:len(commaExpr.List)-1]
					}
				}
			}
		} else if blockType == iterationBlock {
			if branchStmt, ok := list[j-1].(*js.BranchStmt); ok && branchStmt.Type == js.ContinueToken && branchStmt.Label == nil {
				j--
			}
		}
	}
	return list[:j]
}
