// Copyright (c) 2019-2024 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.token

fn (mut c Checker) interface_decl(mut node ast.InterfaceDecl) {
	c.check_valid_pascal_case(node.name, 'interface name', node.pos)
	mut decl_sym := c.table.sym(node.typ)
	is_js := node.language == .js
	if mut decl_sym.info is ast.Interface {
		mut has_generic_types := false
		if node.embeds.len > 0 {
			all_embeds := c.expand_iface_embeds(node, 0, node.embeds)
			// eprintln('> node.name: $node.name | node.embeds.len: $node.embeds.len | all_embeds: $all_embeds.len')
			node.embeds = all_embeds
			mut emnames := map[string]int{}
			mut emnames_ds := map[string]bool{}
			mut emnames_ds_info := map[string]bool{}
			mut efnames := map[string]int{}
			mut efnames_ds_info := map[string]bool{}
			for i, m in node.methods {
				emnames[m.name] = i
				emnames_ds[m.name] = true
				emnames_ds_info[m.name] = true
			}
			for i, f in node.fields {
				efnames[f.name] = i
				efnames_ds_info[f.name] = true
			}
			for embed in all_embeds {
				isym := c.table.sym(embed.typ)
				if embed.typ.has_flag(.generic) {
					has_generic_types = true
				}
				if isym.kind != .interface {
					c.error('interface `${node.name}` tries to embed `${isym.name}`, but `${isym.name}` is not an interface, but `${isym.kind}`',
						embed.pos)
					continue
				}
				// Ensure each generic type of the embed was declared in the interface's definition
				if node.generic_types.len > 0 && embed.typ.has_flag(.generic) {
					embed_generic_names := c.table.generic_type_names(embed.typ)
					node_generic_names := node.generic_types.map(c.table.type_to_str(it))
					for name in embed_generic_names {
						if name !in node_generic_names {
							interface_generic_names := node_generic_names.join(', ')
							c.error('generic type name `${name}` is not mentioned in interface `${node.name}<${interface_generic_names}>`',
								embed.pos)
						}
					}
				}
				isym_info := isym.info as ast.Interface
				for f in isym_info.fields {
					if !efnames_ds_info[f.name] {
						efnames_ds_info[f.name] = true
						decl_sym.info.fields << f
					}
				}
				for m in isym_info.methods {
					if !emnames_ds_info[m.name] {
						emnames_ds_info[m.name] = true
						decl_sym.info.methods << m.new_method_with_receiver_type(node.typ)
					}
				}
				for m in isym.methods {
					if !emnames_ds[m.name] {
						emnames_ds[m.name] = true
						decl_sym.methods << m.new_method_with_receiver_type(node.typ)
					}
				}
				if embed_decl := c.table.interfaces[embed.typ] {
					for f in embed_decl.fields {
						if f.name in efnames {
							// already existing field name, check for conflicts
							ifield := node.fields[efnames[f.name]]
							if field := c.table.find_field_with_embeds(isym, f.name) {
								if ifield.typ != field.typ {
									exp := c.table.type_to_str(ifield.typ)
									got := c.table.type_to_str(field.typ)
									c.error('embedded interface `${embed_decl.name}` conflicts existing field: `${ifield.name}`, expecting type: `${exp}`, got type: `${got}`',
										ifield.pos)
								}
							}
						} else {
							efnames[f.name] = node.fields.len
							node.fields << f
						}
					}
					for m in embed_decl.methods {
						if m.name in emnames {
							// already existing method name, check for conflicts
							imethod := node.methods[emnames[m.name]]
							if em_fn := decl_sym.find_method(imethod.name) {
								if m_fn := isym.find_method(m.name) {
									msg := c.table.is_same_method(m_fn, em_fn)
									if msg.len > 0 {
										em_sig := c.table.fn_signature(em_fn, skip_receiver: true)
										m_sig := c.table.fn_signature(m_fn, skip_receiver: true)
										c.error('embedded interface `${embed_decl.name}` causes conflict: ${msg}, for interface method `${em_sig}` vs `${m_sig}`',
											imethod.pos)
									}
								}
							}
						} else {
							emnames[m.name] = node.methods.len
							mut new_method := m.new_method_with_receiver_type(node.typ)
							new_method.pos = embed.pos
							node.methods << new_method
						}
					}
				}
			}
		}
		for i, method in node.methods {
			if node.language == .v {
				c.check_valid_snake_case(method.name, 'method name', method.pos)
			}
			if !c.ensure_type_exists(method.return_type, method.return_type_pos) {
				continue
			}
			if is_js {
				mtyp := c.table.sym(method.return_type)
				if !mtyp.is_js_compatible() {
					c.error('method ${method.name} returns non JS type', method.pos)
				}
			}
			if method.return_type.has_flag(.generic) {
				has_generic_types = true
				// Ensure each generic type of the method was declared in the interface's definition
				if node.generic_types.len > 0 {
					method_generic_names := c.table.generic_type_names(method.return_type)
					node_generic_names := node.generic_types.map(c.table.type_to_str(it))
					for name in method_generic_names {
						if name !in node_generic_names {
							interface_generic_names := node_generic_names.join(', ')
							c.error('generic type name `${name}` is not mentioned in interface `${node.name}<${interface_generic_names}>`',
								method.return_type_pos)
						}
					}
				}
			} else if !method.return_type.has_option_or_result() {
				ret_sym := c.table.sym(method.return_type)
				if ret_sym.info is ast.ArrayFixed && !ret_sym.info.is_fn_ret {
					c.cast_to_fixed_array_ret(method.return_type, ret_sym)
				} else if ret_sym.info is ast.Alias {
					parent_sym := c.table.sym(ret_sym.info.parent_type)
					if parent_sym.info is ast.ArrayFixed && !parent_sym.info.is_fn_ret {
						c.cast_to_fixed_array_ret(ret_sym.info.parent_type, parent_sym)
					}
				}
			}
			for j, param in method.params {
				if j == 0 && is_js {
					continue // no need to check first param
				}
				if param.typ.has_flag(.generic) {
					has_generic_types = true
				}
				if !c.ensure_type_exists(param.typ, param.pos) {
					continue
				}
				if reserved_type_names_chk.matches(param.name) {
					c.error('invalid use of reserved type `${param.name}` as a parameter name',
						param.pos)
				}
				// Ensure each generic type of the method was declared in the interface's definition
				if node.generic_types.len > 0 && param.typ.has_flag(.generic) {
					method_generic_names := c.table.generic_type_names(param.typ)
					node_generic_names := node.generic_types.map(c.table.type_to_str(it))
					for name in method_generic_names {
						if name !in node_generic_names {
							interface_generic_names := node_generic_names.join(', ')
							c.error('generic type name `${name}` is not mentioned in interface `${node.name}<${interface_generic_names}>`',
								param.type_pos)
						}
					}
				}
				if is_js {
					ptyp := c.table.sym(param.typ)
					if !ptyp.is_js_compatible() && !(j == method.params.len - 1
						&& method.is_variadic) {
						c.error('method `${method.name}` accepts non JS type as parameter',
							method.pos)
					}
				}
			}
			for field in node.fields {
				field_sym := c.table.sym(field.typ)
				if field.name == method.name && field_sym.kind == .function {
					c.error('type `${decl_sym.name}` has both field and method named `${method.name}`',
						method.pos)
				}
			}
			for j in 0 .. i {
				if method.name == node.methods[j].name {
					c.error('duplicate method name `${method.name}`', method.pos)
				}
			}
		}
		for i, field in node.fields {
			if node.language == .v {
				c.check_valid_snake_case(field.name, 'field name', field.pos)
			}
			if !c.ensure_type_exists(field.typ, field.pos) {
				continue
			}
			if field.typ.has_flag(.generic) {
				has_generic_types = true
			}
			if is_js {
				tsym := c.table.sym(field.typ)
				if !tsym.is_js_compatible() {
					c.error('field `${field.name}` uses non JS type', field.pos)
				}
			}
			if field.typ == node.typ && node.language != .js {
				c.error('recursive interface fields are not allowed because they cannot be initialised',
					field.type_pos)
			}
			for j in 0 .. i {
				if field.name == node.fields[j].name {
					c.error('field name `${field.name}` duplicate', field.pos)
				}
			}
		}
		if node.generic_types.len == 0 && has_generic_types {
			c.error('generic interface `${node.name}` declaration must specify the generic type names, e.g. ${node.name}[T]',
				node.pos)
		}
	}
}

fn (mut c Checker) unwrap_generic_interface(typ ast.Type, interface_type ast.Type, pos token.Pos) ast.Type {
	utyp := c.unwrap_generic(typ)
	typ_sym := c.table.sym(utyp)
	mut inter_sym := c.table.sym(interface_type)

	if mut inter_sym.info is ast.Interface {
		if inter_sym.info.is_generic {
			mut inferred_types := []ast.Type{}
			generic_names := inter_sym.info.generic_types.map(c.table.get_type_name(it))
			// inferring interface generic types
			for gt_name in generic_names {
				mut inferred_type := ast.void_type
				for ifield in inter_sym.info.fields {
					if ifield.typ.has_flag(.generic) && c.table.get_type_name(ifield.typ) == gt_name {
						if field := c.table.find_field_with_embeds(typ_sym, ifield.name) {
							inferred_type = field.typ
						}
					}
				}
				for imethod in inter_sym.info.methods {
					method := typ_sym.find_method_with_generic_parent(imethod.name) or {
						c.error('can not find method `${imethod.name}` on `${typ_sym.name}`, needed for interface: `${inter_sym.name}`',
							pos)
						return 0
					}
					if imethod.return_type.has_flag(.generic) {
						imret_sym := c.table.sym(imethod.return_type)
						mret_sym := c.table.sym(method.return_type)
						if method.return_type == ast.void_type
							&& imethod.return_type != method.return_type {
							c.error('interface method `${imethod.name}` returns `${imret_sym.name}`, but implementation method `${method.name}` returns no value',
								pos)
							return 0
						}
						if imethod.return_type == ast.void_type
							&& imethod.return_type != method.return_type {
							c.error('interface method `${imethod.name}` returns no value, but implementation method `${method.name}` returns `${mret_sym.name}`',
								pos)
							return 0
						}
						if imret_sym.info is ast.MultiReturn && mret_sym.info is ast.MultiReturn {
							for i, mr_typ in imret_sym.info.types {
								if mr_typ.has_flag(.generic)
									&& c.table.get_type_name(mr_typ) == gt_name {
									inferred_type = mret_sym.info.types[i]
								}
							}
						} else if c.table.get_type_name(imethod.return_type) == gt_name {
							mut ret_typ := method.return_type
							if imethod.return_type.has_flag(.option) {
								ret_typ = ret_typ.clear_flag(.option)
							} else if imethod.return_type.has_flag(.result) {
								ret_typ = ret_typ.clear_flag(.result)
							}
							inferred_type = ret_typ
						} else if imret_sym.info is ast.SumType && mret_sym.info is ast.SumType {
							im_generic_names := imret_sym.info.generic_types.map(c.table.sym(it).name)
							if gt_name in im_generic_names
								&& imret_sym.info.generic_types.len == mret_sym.info.concrete_types.len {
								idx := im_generic_names.index(gt_name)
								inferred_type = mret_sym.info.concrete_types[idx]
							}
						} else if imret_sym.info is ast.Interface && mret_sym.info is ast.Interface {
							im_generic_names := imret_sym.info.generic_types.map(c.table.sym(it).name)
							if gt_name in im_generic_names
								&& imret_sym.info.generic_types.len == mret_sym.info.concrete_types.len {
								idx := im_generic_names.index(gt_name)
								inferred_type = mret_sym.info.concrete_types[idx]
							}
						} else if imret_sym.info is ast.Struct && mret_sym.info is ast.Struct {
							im_generic_names := imret_sym.info.generic_types.map(c.table.sym(it).name)
							if gt_name in im_generic_names
								&& imret_sym.info.generic_types.len == mret_sym.info.concrete_types.len {
								idx := im_generic_names.index(gt_name)
								inferred_type = mret_sym.info.concrete_types[idx]
							}
						}
					}
					for i, iparam in imethod.params {
						param := method.params[i] or { ast.Param{} }
						mut need_inferred_type := false
						if !iparam.typ.has_flag(.generic) && iparam.typ == param.typ
							&& imethod.return_type == method.return_type
							&& imethod.name == method.name {
							need_inferred_type = true
						}
						if iparam.typ.has_flag(.generic) || need_inferred_type {
							param_sym := c.table.sym(iparam.typ)
							arg_sym := c.table.sym(param.typ)
							if c.table.get_type_name(iparam.typ) == gt_name || need_inferred_type {
								inferred_type = param.typ
							} else if arg_sym.info is ast.Array && param_sym.info is ast.Array {
								mut arg_elem_typ, mut param_elem_typ := arg_sym.info.elem_type, param_sym.info.elem_type
								mut arg_elem_sym, mut param_elem_sym := c.table.sym(arg_elem_typ), c.table.sym(param_elem_typ)
								for {
									if mut arg_elem_sym.info is ast.Array
										&& mut param_elem_sym.info is ast.Array {
										arg_elem_typ, param_elem_typ = arg_elem_sym.info.elem_type, param_elem_sym.info.elem_type
										arg_elem_sym, param_elem_sym = c.table.sym(arg_elem_typ), c.table.sym(param_elem_typ)
									} else {
										if param_elem_sym.name == gt_name {
											inferred_type = arg_elem_typ
										}
										break
									}
								}
							} else if arg_sym.info is ast.ArrayFixed
								&& param_sym.info is ast.ArrayFixed {
								mut arg_elem_typ, mut param_elem_typ := arg_sym.info.elem_type, param_sym.info.elem_type
								mut arg_elem_sym, mut param_elem_sym := c.table.sym(arg_elem_typ), c.table.sym(param_elem_typ)
								for {
									if mut arg_elem_sym.info is ast.ArrayFixed
										&& mut param_elem_sym.info is ast.ArrayFixed {
										arg_elem_typ, param_elem_typ = arg_elem_sym.info.elem_type, param_elem_sym.info.elem_type
										arg_elem_sym, param_elem_sym = c.table.sym(arg_elem_typ), c.table.sym(param_elem_typ)
									} else {
										if param_elem_sym.name == gt_name {
											inferred_type = arg_elem_typ
										}
										break
									}
								}
							} else if arg_sym.info is ast.Map && param_sym.info is ast.Map {
								if param_sym.info.key_type.has_flag(.generic)
									&& c.table.sym(param_sym.info.key_type).name == gt_name {
									inferred_type = arg_sym.info.key_type
								}
								if param_sym.info.value_type.has_flag(.generic)
									&& c.table.sym(param_sym.info.value_type).name == gt_name {
									inferred_type = arg_sym.info.value_type
								}
							}
						}
					}
				}
				if inferred_type == ast.void_type {
					c.error('could not infer generic type `${gt_name}` in interface `${inter_sym.name}`',
						pos)
					return interface_type
				}
				inferred_types << inferred_type
			}
			// add concrete types to method
			for imethod in inter_sym.info.methods {
				im_fkey := imethod.fkey()
				if inferred_types !in c.table.fn_generic_types[im_fkey] {
					c.table.fn_generic_types[im_fkey] << inferred_types
				}
			}
			inter_sym.info.concrete_types = inferred_types
			return c.table.unwrap_generic_type(interface_type, generic_names, inter_sym.info.concrete_types)
		}
	}
	return interface_type
}
