package types

import (
	"strconv"
	"strings"

	"github.com/marzeq/qk/shared"
)

type Type interface {
	Equals(Type) bool
	CanCoerceTo(Type) bool
	CanCastTo(Type) bool
	String() string
}

type PrimitiveType string

const (
	PrimitiveI8  PrimitiveType = "i8"
	PrimitiveI16 PrimitiveType = "i16"
	PrimitiveI32 PrimitiveType = "i32"
	PrimitiveI64 PrimitiveType = "i64"

	PrimitiveU8  PrimitiveType = "u8"
	PrimitiveU16 PrimitiveType = "u16"
	PrimitiveU32 PrimitiveType = "u32"
	PrimitiveU64 PrimitiveType = "u64"

	PrimitiveF32 PrimitiveType = "f32"
	PrimitiveF64 PrimitiveType = "f64"

	PrimitiveIsz PrimitiveType = "isz"
	PrimitiveUsz PrimitiveType = "usz"

	PrimitiveVoid PrimitiveType = "void"

	PrimitiveChar PrimitiveType = "char"

	PrimitiveBool PrimitiveType = "bool"
)

func IsSigned(t Type) bool {
	return t == PrimitiveI8 || t == PrimitiveI16 || t == PrimitiveI32 || t == PrimitiveI64 || t == PrimitiveIsz
}

func IsUnsigned(t Type) bool {
	return t == PrimitiveU8 || t == PrimitiveU16 || t == PrimitiveU32 || t == PrimitiveU64 || t == PrimitiveUsz
}

func IsInteger(t Type) bool {
	return IsSigned(t) || IsUnsigned(t) || t.Equals(UntypedInt{})
}

func IsFloat(t Type) bool {
	return t == PrimitiveF32 || t == PrimitiveF64 || t.Equals(UntypedFloat{})
}

func IsNumeric(t Type) bool {
	return IsInteger(t) || IsFloat(t)
}

func IntegerRank(p PrimitiveType) int {
	switch p {
	case PrimitiveI8, PrimitiveU8:
		return 1
	case PrimitiveI16, PrimitiveU16:
		return 2
	case PrimitiveI32, PrimitiveU32:
		return 3
	case PrimitiveI64, PrimitiveU64:
		return 4
	case PrimitiveIsz, PrimitiveUsz:
		return 5
	default:
		return 0
	}
}

func FloatRank(p PrimitiveType) int {
	switch p {
	case PrimitiveF32:
		return 1
	case PrimitiveF64:
		return 2
	default:
		return 0
	}
}

func (p PrimitiveType) Equals(other Type) bool {
	if otherPrimitive, ok := other.(PrimitiveType); ok {
		return p == otherPrimitive
	}
	return false
}

func (p PrimitiveType) CanCoerceTo(other Type) bool {
	if p.Equals(other) {
		return true
	}

	otherPrimitive, ok := other.(PrimitiveType)
	if !ok {
		return false
	}

	if IsInteger(p) && IsInteger(otherPrimitive) {
		if p == PrimitiveIsz && IsSigned(otherPrimitive) || otherPrimitive == PrimitiveUsz {
			return true
		}

		if IsSigned(p) && IsSigned(otherPrimitive) {
			return IntegerRank(p) <= IntegerRank(otherPrimitive)
		}
		if IsUnsigned(p) && IsUnsigned(otherPrimitive) {
			return IntegerRank(p) <= IntegerRank(otherPrimitive)
		}
		return false
	}

	if IsInteger(p) && IsFloat(otherPrimitive) {
		return true
	}

	if IsFloat(p) && IsFloat(otherPrimitive) {
		return FloatRank(p) <= FloatRank(otherPrimitive)
	}

	return false
}

func (p PrimitiveType) CanCastTo(other Type) bool {
	if p.Equals(other) {
		return true
	}

	otherPrimitive, ok := other.(PrimitiveType)
	if !ok {
		return false
	}

	if p.Equals(PrimitiveBool) && IsInteger(otherPrimitive) ||
		IsInteger(p) && otherPrimitive.Equals(PrimitiveBool) {
		return true
	}

	if IsNumeric(p) && IsNumeric(otherPrimitive) {
		return true
	}

	if (p == PrimitiveChar && IsInteger(otherPrimitive)) ||
		(otherPrimitive == PrimitiveChar && IsInteger(p)) {
		return true
	}

	if IsInteger(p) {
		if _, ok := other.(PointerType); ok {
			return true
		}
	}

	return false
}

func (p PrimitiveType) String() string {
	return string(p)
}

type StructType struct {
	Fields    []shared.Pair[string, Type]
	Anonymous bool
}

func (s StructType) Equals(other Type) bool {
	otherStruct, ok := other.(StructType)
	if !ok {
		return false
	}

	if len(s.Fields) != len(otherStruct.Fields) {
		return false
	}

	for i, field := range s.Fields {
		otherField := otherStruct.Fields[i]
		if field.L != otherField.L || !field.R.Equals(otherField.R) {
			return false
		}
	}

	return true
}

func (s StructType) CanCoerceTo(other Type) bool {
	otherStruct, ok := other.(StructType)
	if !ok {
		return false
	}

	if s.Anonymous && otherStruct.Anonymous || !s.Anonymous && !otherStruct.Anonymous {
		return s.Equals(other)
	}

	if s.Anonymous {
		return false
	}

	var concrete StructType
	var anonymous StructType
	if s.Anonymous {
		concrete = s
		anonymous = otherStruct
	} else {
		concrete = otherStruct
		anonymous = s
	}

	for _, concreteField := range concrete.Fields {
		found := false
		for _, anonymousField := range anonymous.Fields {
			if concreteField.L == anonymousField.L {
				if !anonymousField.R.CanCoerceTo(concreteField.R) {
					return false
				}
				found = true
				break
			}
		}
		if !found {
			return false
		}
	}

	return true
}

func (s StructType) CanCastTo(other Type) bool {
	return s.CanCoerceTo(other)
}

func (s StructType) String() string {
	var result strings.Builder
	if !s.Anonymous {
		result.WriteString("<unordered>")
	}
	result.WriteString("struct { ")
	for i, field := range s.Fields {
		result.WriteString(field.L + ": " + field.R.String())
		if i < len(s.Fields)-1 {
			result.WriteString(", ")
		}
	}
	result.WriteString(" }")
	return result.String()
}

type PointerType struct {
	Base Type
}

func (p PointerType) Equals(other Type) bool {
	if otherPointer, ok := other.(PointerType); ok {
		return p.Base.Equals(otherPointer.Base)
	}
	return false
}

func (p PointerType) CanCoerceTo(other Type) bool {
	if p.Equals(other) {
		return true
	}

	otherPointer, ok := other.(PointerType)
	if !ok {
		return false
	}

	if p.Base.Equals(PrimitiveVoid) || otherPointer.Base.Equals(PrimitiveVoid) {
		return true
	}
	return p.Base.CanCoerceTo(otherPointer.Base)
}

func (p PointerType) CanCastTo(other Type) bool {
	if p.Equals(other) {
		return true
	}

	switch t := other.(type) {
	case PointerType:
		return true
	case PrimitiveType:
		return IsInteger(t)
	}

	return false
}

func (p PointerType) String() string {
	return "*" + p.Base.String()
}

func IsPointer(t Type) bool {
	_, ok := t.(PointerType)
	return ok
}

type ArrayType struct {
	Base Type
	Size int
}

func (a ArrayType) Equals(other Type) bool {
	otherArray, ok := other.(ArrayType)
	if !ok {
		return false
	}

	if a.Size != otherArray.Size {
		return false
	}

	return a.Base.Equals(otherArray.Base)
}

func (a ArrayType) CanCoerceTo(other Type) bool {
	if a.Equals(other) {
		return true
	}

	otherArray, ok := other.(ArrayType)
	if !ok {
		return false
	}

	if a.Size != otherArray.Size {
		return false
	}

	return a.Base.CanCoerceTo(otherArray.Base)
}

func (a ArrayType) CanCastTo(other Type) bool {
	return a.Equals(other)
}

func (a ArrayType) String() string {
	if a.Size == -1 {
		return "[" + a.Base.String() + "]"
	}
	return "[" + a.Base.String() + ", " + strconv.Itoa(a.Size) + "]"
}

type FunctionType struct {
	Parameters []Type
	ReturnType Type
}

func (f FunctionType) Equals(other Type) bool {
	otherFunction, ok := other.(FunctionType)
	if !ok {
		return false
	}

	if len(f.Parameters) != len(otherFunction.Parameters) {
		return false
	}

	for i, param := range f.Parameters {
		if !param.Equals(otherFunction.Parameters[i]) {
			return false
		}
	}

	return f.ReturnType.Equals(otherFunction.ReturnType)
}

func (f FunctionType) CanCoerceTo(other Type) bool {
	otherFunction, ok := other.(FunctionType)
	if !ok {
		return false
	}

	if len(f.Parameters) != len(otherFunction.Parameters) {
		return false
	}

	for i := range f.Parameters {
		if !otherFunction.Parameters[i].CanCoerceTo(f.Parameters[i]) {
			return false
		}
	}

	return f.ReturnType.CanCoerceTo(otherFunction.ReturnType)
}

func (f FunctionType) CanCastTo(other Type) bool {
	return f.Equals(other)
}

func (f FunctionType) String() string {
	var result strings.Builder
	result.WriteString("(")
	for i, param := range f.Parameters {
		result.WriteString(param.String())
		if i < len(f.Parameters)-1 {
			result.WriteString(", ")
		}
	}
	result.WriteString("): " + f.ReturnType.String())
	return result.String()
}

type ErrorType struct{}

func (e ErrorType) Equals(other Type) bool {
	_, ok := other.(ErrorType)
	return ok
}

func (e ErrorType) CanCoerceTo(other Type) bool {
	return true
}

func (e ErrorType) CanCastTo(other Type) bool {
	return true
}

func (e ErrorType) String() string {
	return "<error>"
}

type UntypedInt struct{}

func (u UntypedInt) Equals(other Type) bool {
	_, ok := other.(UntypedInt)
	return ok
}

func (u UntypedInt) CanCoerceTo(other Type) bool {
	switch t := other.(type) {
	case PrimitiveType:
		return IsInteger(t) || IsFloat(t)
	case UntypedInt:
		return true
	case UntypedFloat:
		return true
	default:
		return false
	}
}

func (u UntypedInt) CanCastTo(other Type) bool {
	switch t := other.(type) {
	case PrimitiveType:
		return IsNumeric(t) || t.Equals(PrimitiveBool)
	default:
		return false
	}
}

func (u UntypedInt) String() string {
	return "<untyped int>"
}

type UntypedFloat struct{}

func (u UntypedFloat) Equals(other Type) bool {
	_, ok := other.(UntypedFloat)
	return ok
}

func (u UntypedFloat) CanCoerceTo(other Type) bool {
	switch t := other.(type) {
	case PrimitiveType:
		return IsFloat(t)
	case UntypedFloat:
		return true
	default:
		return false
	}
}

func (u UntypedFloat) CanCastTo(other Type) bool {
	switch t := other.(type) {
	case PrimitiveType:
		return IsFloat(t) || IsInteger(t)
	default:
		return false
	}
}

func (u UntypedFloat) String() string {
	return "<untyped float>"
}

func IsUntyped(t Type) bool {
	switch t.(type) {
	case UntypedInt, UntypedFloat:
		return true
	}
	return false
}

func PromoteNumeric(a, b Type) Type {
	if a.Equals(b) {
		return a
	}

	if _, ok := a.(UntypedInt); ok {
		return b
	}
	if _, ok := b.(UntypedInt); ok {
		return a
	}

	if _, ok := a.(UntypedFloat); ok {
		return b
	}
	if _, ok := b.(UntypedFloat); ok {
		return a
	}

	pa, aok := a.(PrimitiveType)
	pb, bok := b.(PrimitiveType)

	if !aok || !bok {
		return ErrorType{}
	}

	if IsFloat(pa) || IsFloat(pb) {
		if pa == PrimitiveF64 || pb == PrimitiveF64 {
			return PrimitiveF64
		}
		return PrimitiveF32
	}

	return PromoteIntegers(pa, pb)
}

func PromoteIntegers(a, b PrimitiveType) Type {
	if IsSigned(a) && IsSigned(b) {
		return widerSigned(a, b)
	}

	if IsUnsigned(a) && IsUnsigned(b) {
		return widerUnsigned(a, b)
	}

	return ErrorType{}
}

func widerSigned(a, b PrimitiveType) PrimitiveType {
	if !IsSigned(a) || !IsSigned(b) {
		panic("widerSigned called with non-signed types")
	}

	if IntegerRank(a) >= IntegerRank(b) {
		return a
	}
	return b
}

func widerUnsigned(a, b PrimitiveType) PrimitiveType {
	if !IsUnsigned(a) || !IsUnsigned(b) {
		panic("widerUnsigned called with non-unsigned types")
	}

	if IntegerRank(a) >= IntegerRank(b) {
		return a
	}
	return b
}

func DefaultUntyped(t Type) Type {
	switch t.(type) {
	case UntypedInt:
		return PrimitiveI32
	case UntypedFloat:
		return PrimitiveF32
	default:
		return t
	}
}

type UnknownType struct{}

func (u UnknownType) Equals(other Type) bool      { return false }
func (u UnknownType) CanCoerceTo(other Type) bool { return false }
func (u UnknownType) CanCastTo(other Type) bool   { return false }
func (u UnknownType) String() string              { return "<unknown>" }