// 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

pub fn (mut c Checker) struct_decl(mut node ast.StructDecl) {
	if node.language == .v && !c.is_builtin_mod {
		c.check_valid_pascal_case(node.name, 'struct name', node.pos)
	}
	mut struct_sym, struct_typ_idx := c.table.find_sym_and_type_idx(node.name)
	mut has_generic_types := false
	if mut struct_sym.info is ast.Struct {
		for embed in node.embeds {
			if embed.typ.has_flag(.generic) {
				has_generic_types = true
			}
			embed_sym := c.table.sym(embed.typ)
			if embed_sym.kind != .struct_ {
				c.error('`$embed_sym.name` is not a struct', embed.pos)
			} else {
				info := embed_sym.info as ast.Struct
				if info.is_heap && !embed.typ.is_ptr() {
					struct_sym.info.is_heap = true
				}
			}
		}
		for attr in node.attrs {
			if attr.name == 'typedef' && node.language != .c {
				c.error('`typedef` attribute can only be used with C structs', node.pos)
			}
		}
		for i, field in node.fields {
			if field.typ == ast.any_type {
				c.error('struct field cannot be the `any` type, use generics instead',
					field.type_pos)
			}
			c.ensure_type_exists(field.typ, field.type_pos) or { return }
			if field.typ.has_flag(.generic) {
				has_generic_types = true
			}
			if node.language == .v {
				c.check_valid_snake_case(field.name, 'field name', field.pos)
			}
			sym := c.table.sym(field.typ)
			for j in 0 .. i {
				if field.name == node.fields[j].name {
					c.error('field name `$field.name` duplicate', field.pos)
				}
			}
			if field.typ != 0 {
				if !field.typ.is_ptr() {
					if c.table.unaliased_type(field.typ) == struct_typ_idx {
						c.error('field `$field.name` is part of `$node.name`, they can not both have the same type',
							field.type_pos)
					}
				}
			}
			if sym.kind == .struct_ {
				info := sym.info as ast.Struct
				if info.is_heap && !field.typ.is_ptr() {
					struct_sym.info.is_heap = true
				}
			}
			if field.has_default_expr {
				c.expected_type = field.typ
				mut field_expr_type := c.expr(field.default_expr)
				if !field.typ.has_flag(.optional) {
					c.check_expr_opt_call(field.default_expr, field_expr_type)
				}
				struct_sym.info.fields[i].default_expr_typ = field_expr_type
				c.check_expected(field_expr_type, field.typ) or {
					if sym.kind == .interface_
						&& c.type_implements(field_expr_type, field.typ, field.pos) {
						if !field_expr_type.is_ptr() && !field_expr_type.is_pointer()
							&& !c.inside_unsafe {
							field_expr_type_sym := c.table.sym(field_expr_type)
							if field_expr_type_sym.kind != .interface_ {
								c.mark_as_referenced(mut &node.fields[i].default_expr,
									true)
							}
						}
					} else {
						c.error('incompatible initializer for field `$field.name`: $err.msg',
							field.default_expr.pos())
					}
				}
				// Check for unnecessary inits like ` = 0` and ` = ''`
				if field.typ.is_ptr() {
					continue
				}
				if field.default_expr is ast.IntegerLiteral {
					if field.default_expr.val == '0' {
						c.warn('unnecessary default value of `0`: struct fields are zeroed by default',
							field.default_expr.pos)
					}
				} else if field.default_expr is ast.StringLiteral {
					if field.default_expr.val == '' {
						c.warn("unnecessary default value of '': struct fields are zeroed by default",
							field.default_expr.pos)
					}
				} else if field.default_expr is ast.BoolLiteral {
					if field.default_expr.val == false {
						c.warn('unnecessary default value `false`: struct fields are zeroed by default',
							field.default_expr.pos)
					}
				}
			}
		}
		if node.generic_types.len == 0 && has_generic_types {
			c.error('generic struct declaration must specify the generic type names, e.g. Foo<T>',
				node.pos)
		}
	}
}

pub fn (mut c Checker) struct_init(mut node ast.StructInit) ast.Type {
	if node.typ == ast.void_type {
		// Short syntax `({foo: bar})`
		if c.expected_type == ast.void_type {
			c.error('unexpected short struct syntax', node.pos)
			return ast.void_type
		}
		sym := c.table.sym(c.expected_type)
		if sym.kind == .array {
			node.typ = c.table.value_type(c.expected_type)
		} else {
			node.typ = c.expected_type
		}
	}
	struct_sym := c.table.sym(node.typ)
	if struct_sym.info is ast.Struct {
		if struct_sym.info.generic_types.len > 0 && struct_sym.info.concrete_types.len == 0
			&& c.table.cur_concrete_types.len == 0 {
			c.error('generic struct init must specify type parameter, e.g. Foo<int>',
				node.pos)
		}
		if node.generic_types.len > 0 && struct_sym.info.generic_types != node.generic_types {
			c.table.replace_generic_type(node.typ, node.generic_types)
		}
	} else if struct_sym.info is ast.Alias {
		parent_sym := c.table.sym(struct_sym.info.parent_type)
		// e.g. ´x := MyMapAlias{}´, should be a cast to alias type ´x := MyMapAlias(map[...]...)´
		if parent_sym.kind == .map {
			alias_str := c.table.type_to_str(node.typ)
			map_str := c.table.type_to_str(struct_sym.info.parent_type)
			c.error('direct map alias init is not possible, use `${alias_str}($map_str{})` instead',
				node.pos)
			return ast.void_type
		}
	}
	// register generic struct type when current fn is generic fn
	if c.table.cur_fn.generic_names.len > 0 {
		c.table.unwrap_generic_type(node.typ, c.table.cur_fn.generic_names, c.table.cur_concrete_types)
	}
	c.ensure_type_exists(node.typ, node.pos) or {}
	type_sym := c.table.sym(node.typ)
	if !c.inside_unsafe && type_sym.kind == .sum_type {
		c.note('direct sum type init (`x := SumType{}`) will be removed soon', node.pos)
	}
	// Make sure the first letter is capital, do not allow e.g. `x := string{}`,
	// but `x := T{}` is ok.
	if !c.is_builtin_mod && !c.inside_unsafe && type_sym.language == .v
		&& c.table.cur_concrete_types.len == 0 {
		pos := type_sym.name.last_index('.') or { -1 }
		first_letter := type_sym.name[pos + 1]
		if !first_letter.is_capital() {
			c.error('cannot initialize builtin type `$type_sym.name`', node.pos)
		}
	}
	if type_sym.kind == .sum_type && node.fields.len == 1 {
		sexpr := node.fields[0].expr.str()
		c.error('cast to sum type using `${type_sym.name}($sexpr)` not `$type_sym.name{$sexpr}`',
			node.pos)
	}
	if type_sym.kind == .interface_ && type_sym.language != .js {
		c.error('cannot instantiate interface `$type_sym.name`', node.pos)
	}
	if type_sym.info is ast.Alias {
		if type_sym.info.parent_type.is_number() {
			c.error('cannot instantiate number type alias `$type_sym.name`', node.pos)
			return ast.void_type
		}
	}
	// allow init structs from generic if they're private except the type is from builtin module
	if !type_sym.is_public && type_sym.kind != .placeholder && type_sym.language != .c
		&& (type_sym.mod != c.mod && !(node.typ.has_flag(.generic) && type_sym.mod != 'builtin')) {
		c.error('type `$type_sym.name` is private', node.pos)
	}
	if type_sym.kind == .struct_ {
		info := type_sym.info as ast.Struct
		if info.attrs.len > 0 && info.attrs[0].name == 'noinit' && type_sym.mod != c.mod {
			c.error('struct `$type_sym.name` is declared with a `[noinit]` attribute, so ' +
				'it cannot be initialized with `$type_sym.name{}`', node.pos)
		}
	}
	if type_sym.name.len == 1 && c.table.cur_fn.generic_names.len == 0 {
		c.error('unknown struct `$type_sym.name`', node.pos)
		return 0
	}
	match type_sym.kind {
		.placeholder {
			c.error('unknown struct: $type_sym.name', node.pos)
			return ast.void_type
		}
		// string & array are also structs but .kind of string/array
		.struct_, .string, .array, .alias {
			mut info := ast.Struct{}
			if type_sym.kind == .alias {
				info_t := type_sym.info as ast.Alias
				sym := c.table.sym(info_t.parent_type)
				if sym.kind == .placeholder { // pending import symbol did not resolve
					c.error('unknown struct: $type_sym.name', node.pos)
					return ast.void_type
				}
				if sym.kind == .struct_ {
					info = sym.info as ast.Struct
				} else {
					c.error('alias type name: $sym.name is not struct type', node.pos)
				}
			} else {
				info = type_sym.info as ast.Struct
			}
			if node.is_short {
				exp_len := info.fields.len
				got_len := node.fields.len
				if exp_len != got_len {
					amount := if exp_len < got_len { 'many' } else { 'few' }
					c.error('too $amount fields in `$type_sym.name` literal (expecting $exp_len, got $got_len)',
						node.pos)
				}
			}
			mut inited_fields := []string{}
			for i, mut field in node.fields {
				mut field_info := ast.StructField{}
				mut field_name := ''
				if node.is_short {
					if i >= info.fields.len {
						// It doesn't make sense to check for fields that don't exist.
						// We should just stop here.
						break
					}
					field_info = info.fields[i]
					field_name = field_info.name
					node.fields[i].name = field_name
				} else {
					field_name = field.name
					mut exists := true
					field_info = c.table.find_field_with_embeds(type_sym, field_name) or {
						exists = false
						ast.StructField{}
					}
					if !exists {
						c.error('unknown field `$field.name` in struct literal of type `$type_sym.name`',
							field.pos)
						continue
					}
					if field_name in inited_fields {
						c.error('duplicate field name in struct literal: `$field_name`',
							field.pos)
						continue
					}
				}
				mut expr_type := ast.Type(0)
				mut expected_type := ast.Type(0)
				inited_fields << field_name
				field_type_sym := c.table.sym(field_info.typ)
				expected_type = field_info.typ
				c.expected_type = expected_type
				expr_type = c.expr(field.expr)
				if !field_info.typ.has_flag(.optional) {
					expr_type = c.check_expr_opt_call(field.expr, expr_type)
				}
				expr_type_sym := c.table.sym(expr_type)
				if field_type_sym.kind == .interface_ {
					if c.type_implements(expr_type, field_info.typ, field.pos) {
						if !expr_type.is_ptr() && !expr_type.is_pointer()
							&& expr_type_sym.kind != .interface_ && !c.inside_unsafe {
							c.mark_as_referenced(mut &field.expr, true)
						}
					}
				} else if expr_type != ast.void_type && expr_type_sym.kind != .placeholder {
					c.check_expected(c.unwrap_generic(expr_type), c.unwrap_generic(field_info.typ)) or {
						c.error('cannot assign to field `$field_info.name`: $err.msg',
							field.pos)
					}
				}
				if field_info.typ.has_flag(.shared_f) {
					if !expr_type.has_flag(.shared_f) && expr_type.is_ptr() {
						c.error('`shared` field must be initialized with `shared` or value',
							field.pos)
					}
				} else {
					if field_info.typ.is_ptr() && !expr_type.is_ptr() && !expr_type.is_pointer()
						&& !expr_type.is_number() {
						c.error('reference field must be initialized with reference',
							field.pos)
					}
				}
				node.fields[i].typ = expr_type
				node.fields[i].expected_type = field_info.typ

				if field_info.typ.has_flag(.optional) {
					c.error('field `$field_info.name` is optional, but initialization of optional fields currently unsupported',
						field.pos)
				}
				if expr_type.is_ptr() && expected_type.is_ptr() {
					if mut field.expr is ast.Ident {
						if mut field.expr.obj is ast.Var {
							mut obj := unsafe { &field.expr.obj }
							if c.fn_scope != voidptr(0) {
								obj = c.fn_scope.find_var(obj.name) or { obj }
							}
							if obj.is_stack_obj && !c.inside_unsafe {
								sym := c.table.sym(obj.typ.set_nr_muls(0))
								if !sym.is_heap() && !c.pref.translated && !c.file.is_translated {
									suggestion := if sym.kind == .struct_ {
										'declaring `$sym.name` as `[heap]`'
									} else {
										'wrapping the `$sym.name` object in a `struct` declared as `[heap]`'
									}
									c.error('`$field.expr.name` cannot be assigned outside `unsafe` blocks as it might refer to an object stored on stack. Consider ${suggestion}.',
										field.expr.pos)
								}
							}
						}
					}
				}
			}
			// Check uninitialized refs/sum types
			for i, field in info.fields {
				if field.name in inited_fields {
					continue
				}
				if field.has_default_expr {
					if field.default_expr_typ == 0 {
						if field.default_expr is ast.StructInit {
							idx := c.table.find_type_idx(field.default_expr.typ_str)
							if idx != 0 {
								info.fields[i].default_expr_typ = ast.new_type(idx)
							}
						} else {
							if const_field := c.table.global_scope.find_const('$field.default_expr') {
								info.fields[i].default_expr_typ = const_field.typ
							}
						}
					}
					continue
				}
				if field.typ.is_ptr() && !field.typ.has_flag(.shared_f) && !node.has_update_expr
					&& !c.pref.translated && !c.file.is_translated {
					c.error('reference field `${type_sym.name}.$field.name` must be initialized',
						node.pos)
				}
				// Do not allow empty uninitialized interfaces
				sym := c.table.sym(field.typ)
				mut has_noinit := false
				for attr in field.attrs {
					if attr.name == 'noinit' {
						has_noinit = true
						break
					}
				}
				if sym.kind == .interface_ && (!has_noinit && sym.language != .js) {
					// TODO: should be an error instead, but first `ui` needs updating.
					c.note('interface field `${type_sym.name}.$field.name` must be initialized',
						node.pos)
				}
				// Do not allow empty uninitialized sum types
				/*
				sym := c.table.sym(field.typ)
				if sym.kind == .sum_type {
					c.warn('sum type field `${type_sym.name}.$field.name` must be initialized',
						node.pos)
				}
				*/
				// Check for `[required]` struct attr
				if field.attrs.contains('required') && !node.is_short && !node.has_update_expr {
					mut found := false
					for init_field in node.fields {
						if field.name == init_field.name {
							found = true
							break
						}
					}
					if !found {
						c.error('field `${type_sym.name}.$field.name` must be initialized',
							node.pos)
					}
				}
			}
		}
		else {}
	}
	if node.has_update_expr {
		update_type := c.expr(node.update_expr)
		node.update_expr_type = update_type
		if c.table.type_kind(update_type) != .struct_ {
			s := c.table.type_to_str(update_type)
			c.error('expected struct, found `$s`', node.update_expr.pos())
		} else if update_type != node.typ {
			from_sym := c.table.sym(update_type)
			to_sym := c.table.sym(node.typ)
			from_info := from_sym.info as ast.Struct
			to_info := to_sym.info as ast.Struct
			// TODO this check is too strict
			if !c.check_struct_signature(from_info, to_info) {
				c.error('struct `$from_sym.name` is not compatible with struct `$to_sym.name`',
					node.update_expr.pos())
			}
		}
		if !node.update_expr.is_lvalue() {
			// cgen will repeat `update_expr` for each field
			// so enforce an lvalue for efficiency
			c.error('expression is not an lvalue', node.update_expr.pos())
		}
	}
	return node.typ
}
