// Copyright (c) 2019-2022 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
module checker

import v.ast
import v.pref
import v.token

pub fn (mut c Checker) if_expr(mut node ast.IfExpr) ast.Type {
	if_kind := if node.is_comptime { '\$if' } else { 'if' }
	mut node_is_expr := false
	if node.branches.len > 0 && node.has_else {
		stmts := node.branches[0].stmts
		if stmts.len > 0 && stmts[stmts.len - 1] is ast.ExprStmt
			&& (stmts[stmts.len - 1] as ast.ExprStmt).typ != ast.void_type {
			node_is_expr = true
		}
	}
	if c.expected_type == ast.void_type && node_is_expr {
		c.expected_type = c.expected_or_type
	}
	expr_required := c.expected_type != ast.void_type
	former_expected_type := c.expected_type
	if node_is_expr {
		c.expected_expr_type = c.expected_type
		defer {
			c.expected_expr_type = ast.void_type
		}
	}
	node.typ = ast.void_type
	mut nbranches_with_return := 0
	mut nbranches_without_return := 0
	mut should_skip := false // Whether the current branch should be skipped
	mut found_branch := false // Whether a matching branch was found- skip the rest
	mut is_comptime_type_is_expr := false // if `$if T is string`
	for i in 0 .. node.branches.len {
		mut branch := node.branches[i]
		if branch.cond is ast.ParExpr && !c.pref.translated && !c.file.is_translated {
			c.error('unnecessary `()` in `$if_kind` condition, use `$if_kind expr {` instead of `$if_kind (expr) {`.',
				branch.pos)
		}
		if !node.has_else || i < node.branches.len - 1 {
			if node.is_comptime {
				should_skip = c.comptime_if_branch(branch.cond, branch.pos)
				node.branches[i].pkg_exist = !should_skip
			} else {
				// check condition type is boolean
				c.expected_type = ast.bool_type
				cond_typ := c.unwrap_generic(c.expr(branch.cond))
				if (cond_typ.idx() != ast.bool_type_idx || cond_typ.has_flag(.optional))
					&& !c.pref.translated && !c.file.is_translated {
					c.error('non-bool type `${c.table.type_to_str(cond_typ)}` used as if condition',
						branch.cond.pos())
				}
			}
		}
		if node.is_comptime { // Skip checking if needed
			// smartcast field type on comptime if
			mut comptime_field_name := ''
			if mut branch.cond is ast.InfixExpr {
				if branch.cond.op == .key_is {
					if branch.cond.right !is ast.TypeNode && branch.cond.right !is ast.ComptimeType {
						c.error('invalid `\$if` condition: expected a type', branch.cond.right.pos())
						return 0
					}
					left := branch.cond.left
					if branch.cond.right is ast.ComptimeType && left is ast.TypeNode {
						is_comptime_type_is_expr = true
						checked_type := c.unwrap_generic(left.typ)
						should_skip = !c.table.is_comptime_type(checked_type, branch.cond.right as ast.ComptimeType)
					} else {
						got_type := c.unwrap_generic((branch.cond.right as ast.TypeNode).typ)
						sym := c.table.sym(got_type)
						if sym.kind == .placeholder || got_type.has_flag(.generic) {
							c.error('unknown type `$sym.name`', branch.cond.right.pos())
						}

						if left is ast.SelectorExpr {
							comptime_field_name = left.expr.str()
							c.comptime_fields_type[comptime_field_name] = got_type
							is_comptime_type_is_expr = true
						} else if branch.cond.right is ast.TypeNode && left is ast.TypeNode
							&& sym.kind == .interface_ {
							is_comptime_type_is_expr = true
							// is interface
							checked_type := c.unwrap_generic(left.typ)
							should_skip = !c.table.does_type_implement_interface(checked_type,
								got_type)
						} else if left is ast.TypeNode {
							is_comptime_type_is_expr = true
							left_type := c.unwrap_generic(left.typ)
							if left_type != got_type {
								should_skip = true
							}
						}
					}
				}
			}
			cur_skip_flags := c.skip_flags
			if found_branch {
				c.skip_flags = true
			} else if should_skip {
				c.skip_flags = true
				should_skip = false // Reset the value of `should_skip` for the next branch
			} else if !is_comptime_type_is_expr {
				found_branch = true // If a branch wasn't skipped, the rest must be
			}
			if c.fn_level == 0 && c.pref.output_cross_c {
				// do not skip any of the branches for top level `$if OS {`
				// statements, in `-os cross` mode
				found_branch = false
				c.skip_flags = false
				c.ct_cond_stack << branch.cond
			}
			if !c.skip_flags {
				if node_is_expr {
					c.stmts_ending_with_expression(branch.stmts)
				} else {
					c.stmts(branch.stmts)
				}
			} else if c.pref.output_cross_c {
				mut is_freestanding_block := false
				if mut branch.cond is ast.Ident {
					if branch.cond.name == 'freestanding' {
						is_freestanding_block = true
					}
				}
				if is_freestanding_block {
					branch.stmts = []
					node.branches[i].stmts = []
				}
				if node_is_expr {
					c.stmts_ending_with_expression(branch.stmts)
				} else {
					c.stmts(branch.stmts)
				}
			} else if !is_comptime_type_is_expr {
				node.branches[i].stmts = []
			}
			if comptime_field_name.len > 0 {
				c.comptime_fields_type[comptime_field_name] = c.comptime_fields_default_type
			}
			c.skip_flags = cur_skip_flags
			if c.fn_level == 0 && c.pref.output_cross_c && c.ct_cond_stack.len > 0 {
				c.ct_cond_stack.delete_last()
			}
		} else {
			// smartcast sumtypes and interfaces when using `is`
			c.smartcast_if_conds(branch.cond, mut branch.scope)
			if node_is_expr {
				c.stmts_ending_with_expression(branch.stmts)
			} else {
				c.stmts(branch.stmts)
			}
			if c.smartcast_mut_pos != token.Pos{} {
				c.smartcast_mut_pos = token.Pos{}
			}
		}
		if expr_required {
			if branch.stmts.len > 0 && branch.stmts[branch.stmts.len - 1] is ast.ExprStmt {
				mut last_expr := branch.stmts[branch.stmts.len - 1] as ast.ExprStmt
				c.expected_type = former_expected_type
				if c.expected_type.has_flag(.optional) {
					if node.typ == ast.void_type {
						node.is_expr = true
						node.typ = c.expected_type
					}
				}
				if c.expected_type.has_flag(.generic) {
					if node.typ == ast.void_type {
						node.is_expr = true
						node.typ = c.unwrap_generic(c.expected_type)
					}
					continue
				}
				last_expr.typ = c.expr(last_expr.expr)
				if !c.check_types(last_expr.typ, node.typ) {
					if node.typ == ast.void_type {
						// first branch of if expression
						node.is_expr = true
						node.typ = last_expr.typ
						continue
					} else if node.typ in [ast.float_literal_type, ast.int_literal_type] {
						if node.typ == ast.int_literal_type {
							if last_expr.typ.is_int() || last_expr.typ.is_float() {
								node.typ = last_expr.typ
								continue
							}
						} else { // node.typ == float_literal
							if last_expr.typ.is_float() {
								node.typ = last_expr.typ
								continue
							}
						}
					}
					if last_expr.typ in [ast.float_literal_type, ast.int_literal_type] {
						if last_expr.typ == ast.int_literal_type {
							if node.typ.is_int() || node.typ.is_float() {
								continue
							}
						} else { // expr_type == float_literal
							if node.typ.is_float() {
								continue
							}
						}
					}
					if node.is_expr && c.table.sym(former_expected_type).kind == .sum_type {
						continue
					}
					if is_noreturn_callexpr(last_expr.expr) {
						continue
					}

					c.error('mismatched types `${c.table.type_to_str(node.typ)}` and `${c.table.type_to_str(last_expr.typ)}`',
						node.pos)
				}
			} else {
				c.error('`$if_kind` expression requires an expression as the last statement of every branch',
					branch.pos)
			}
			for st in branch.stmts {
				// must not contain C statements
				st.check_c_expr() or { c.error('`if` expression branch has $err.msg()', st.pos) }
			}
		}
		if mut branch.cond is ast.IfGuardExpr {
			sym := c.table.sym(branch.cond.expr_type)
			if sym.kind == .multi_return {
				mr_info := sym.info as ast.MultiReturn
				if branch.cond.vars.len != mr_info.types.len {
					c.error('if guard expects $mr_info.types.len variables, but got $branch.cond.vars.len',
						branch.pos)
				} else {
					for vi, var in branch.cond.vars {
						branch.scope.update_var_type(var.name, mr_info.types[vi])
					}
				}
			}
		}
		// Also check for returns inside a comp.if's statements, even if its contents aren't parsed
		if has_return := c.has_return(branch.stmts) {
			if has_return {
				nbranches_with_return++
			} else {
				nbranches_without_return++
			}
		}
	}
	if nbranches_with_return > 0 {
		if nbranches_with_return == node.branches.len {
			// if/else... where all branches returned
			c.returns = true
		}
		if !node.has_else {
			// `if cond { return ... }` means that when cond is false, execution continues
			c.returns = false
		}
		if nbranches_without_return > 0 {
			// some of the branches did not return
			c.returns = false
		}
	}
	// if only untyped literals were given default to int/f64
	if node.typ == ast.int_literal_type {
		node.typ = ast.int_type
	} else if node.typ == ast.float_literal_type {
		node.typ = ast.f64_type
	}
	if expr_required && !node.has_else {
		d := if node.is_comptime { '$' } else { '' }
		c.error('`$if_kind` expression needs `${d}else` clause', node.pos)
	}
	return node.typ
}

fn (mut c Checker) smartcast_if_conds(node ast.Expr, mut scope ast.Scope) {
	if node is ast.InfixExpr {
		if node.op == .and {
			c.smartcast_if_conds(node.left, mut scope)
			c.smartcast_if_conds(node.right, mut scope)
		} else if node.op == .key_is {
			right_expr := node.right
			mut right_type := match right_expr {
				ast.TypeNode {
					right_expr.typ
				}
				ast.None {
					ast.none_type_idx
				}
				else {
					c.error('invalid type `$right_expr`', right_expr.pos())
					ast.Type(0)
				}
			}
			right_type = c.unwrap_generic(right_type)
			if right_type != ast.Type(0) {
				left_sym := c.table.sym(node.left_type)
				right_sym := c.table.sym(right_type)
				expr_type := c.unwrap_generic(c.expr(node.left))
				if left_sym.kind == .interface_ {
					if right_sym.kind != .interface_ {
						c.type_implements(right_type, expr_type, node.pos)
					} else {
						return
					}
				} else if !c.check_types(right_type, expr_type) {
					expect_str := c.table.type_to_str(right_type)
					expr_str := c.table.type_to_str(expr_type)
					c.error('cannot use type `$expect_str` as type `$expr_str`', node.pos)
				}
				if node.left in [ast.Ident, ast.SelectorExpr] && node.right is ast.TypeNode {
					is_variable := if mut node.left is ast.Ident {
						node.left.kind == .variable
					} else {
						true
					}
					if is_variable {
						if left_sym.kind in [.interface_, .sum_type] {
							c.smartcast(node.left, node.left_type, right_type, mut scope)
						}
					}
				}
			}
		}
	} else if node is ast.Likely {
		c.smartcast_if_conds(node.expr, mut scope)
	}
}
