package codegen

import (
	"fmt"
	"strings"

	"github.com/marzeq/quokka/parser"
	"github.com/marzeq/quokka/shared"
	"github.com/marzeq/quokka/typechecker"
)

type (
	Node = parser.Node
	Type = shared.Type
)

type CodeGen struct {
	tmpVarId uint
	tmpLblId uint
	rootNode *Node
	funcSigs map[string]*typechecker.FunctionSig
}

func NewCodeGen(rootNode *Node, funcSigs map[string]*typechecker.FunctionSig) *CodeGen {
	return &CodeGen{
		tmpVarId: 0,
		tmpLblId: 0,
		rootNode: rootNode,
		funcSigs: funcSigs,
	}
}

func (cg *CodeGen) GetTmpVar() string {
	val := fmt.Sprintf("%%___tmpvar%d", cg.tmpVarId)
	cg.tmpVarId++
	return val
}

func (cg *CodeGen) GetTmpLbl() string {
	val := fmt.Sprintf("@___tmplbl%d", cg.tmpLblId)
	cg.tmpLblId++
	return val
}

func (cg *CodeGen) EmitIR() (string, error) {
	ir := ""

	for _, node := range cg.rootNode.Children {
		if node.Type != parser.NODE_TYPE_FUNCTION_DEF {
			return "", shared.NewError(node.Loc, "unexpected token")
		}

		funcIr, err := cg.GenerateFuncIR(node)
		if err != nil {
			return "", err
		}

		ir += funcIr
	}

	// TODO: REMOVE THIS
	ir += "\ndata $printfmt = { b \"%d\\n\", b 0 }"

	return ir, nil
}

func (cg *CodeGen) GenerateFuncIR(funcNode *Node) (string, error) {
	cg.tmpVarId = 0
	cg.tmpLblId = 0
	prologue := ""
	body := ""
	epilogue := ""

	fname := typechecker.IdentToStr(funcNode.Value.(*parser.FunctionValue).Name)
	fsig := cg.funcSigs[fname]

	if fsig.Name == "main" {
		prologue += "export function w $main() {\n@start"
	} else {
		prologue += fmt.Sprintf("function %s $%s(", mapTypeToIRType(fsig.RetType), fsig.Name)
		for i, arg := range fsig.ArgTypes {
			prologue += fmt.Sprintf("%s %%%s", mapTypeToIRType(arg.R), arg.L)
			if i != len(fsig.ArgTypes)-1 {
				prologue += ", "
			}
		}

		prologue += ") {\n@start"

		if fsig.ImplicitReturn {
			epilogue = "\nret"
		}
	}

	epilogue += "\n}\n"

	child := funcNode.Children[0]

	if child.Type == parser.NODE_TYPE_BLOCK {
		b, _, err := cg.GenerateBlockIR(child, "", "")
		if err != nil {
			return "", err
		}
		body = b
	} else {
		val, setups, err := cg.GenerateExprIR(child)
		if err != nil {
			return "", err
		}
		for _, setup := range setups {
			body += "\n" + setup
		}
		body += "\nret " + val
	}

	return prologue + body + epilogue, nil
}

func (cg *CodeGen) GenerateBlockIR(blockNode *Node, loopBegin, loopEnd string) (string, bool, error) {
	body := ""
	endsWithTerminator := false
	for i, node := range blockNode.Children {
		ir, setups, err := cg.GenerateStmtIR(node, i == len(blockNode.Children)-1, loopBegin, loopEnd)
		if err != nil {
			return "", false, err
		}

		for _, setup := range setups {
			body += "\n" + setup
		}
		body += "\n" + ir

		if IsTerminatorInstruction(ir) {
			endsWithTerminator = true
		} else {
			endsWithTerminator = false
		}
	}

	return body, endsWithTerminator, nil
}

func IsTerminatorInstruction(ir string) bool {
	return strings.HasPrefix(ir, "ret") ||
		strings.HasPrefix(ir, "jmp") ||
		strings.HasPrefix(ir, "jnz")
}

func (cg *CodeGen) GenerateStmtIR(stmtNode *Node, last bool, loopBegin, loopEnd string) (string, []string, error) {
	line := ""
	setups := []string{}
	switch stmtNode.Type {
	case parser.NODE_TYPE_CONTROL_KEYWORD:
		switch stmtNode.Value.(string) {
		case "return":
			line = "ret"
			if stmtNode.Right != nil {
				line += " "
				val, stps, err := cg.GenerateExprIR(stmtNode.Right)
				if err != nil {
					return "", nil, err
				}
				line += val
				setups = append(setups, stps...)
			}
		case "break":
			if loopEnd == "" {
				return "", nil, shared.NewError(stmtNode.Loc, "used 'break' keyword outside loop")
			}
			line = fmt.Sprintf("jmp %s", loopEnd)
		case "continue":
			if loopBegin == "" {
				return "", nil, shared.NewError(stmtNode.Loc, "used 'continue' keyword outside loop")
			}
			line = fmt.Sprintf("jmp %s", loopBegin)
		}

	case parser.NODE_TYPE_DECLARATION:
		fallthrough
	case parser.NODE_TYPE_ASSIGNMENT:
		line = fmt.Sprintf("%%%s =%s ", typechecker.IdentToStr(stmtNode.Left), mapTypeToIRType(stmtNode.ExprType))

		val, stps, err := cg.GenerateExprIR(stmtNode.Right)
		val = "copy " + val
		if err != nil {
			return "", nil, err
		}

		line += val
		setups = append(setups, stps...)

	case parser.NODE_TYPE_FUNCTION_CALL:
		fname := typechecker.IdentToStr(stmtNode.Value.(*Node))
		fsig := cg.funcSigs[fname]
		// TODO: REMOVE THIS
		if fname == "_print" {
			line = "call $printf(l $printfmt, ..., "
			if len(stmtNode.Children) != 1 {
				return "", nil, shared.NewError(stmtNode.Loc, "temporary '_print' function expects exactly one argument")
			}
			arg := stmtNode.Children[0]
			line += mapTypeToIRType(arg.ExprType) + " "
			val, stps, err := cg.GenerateExprIR(arg)
			if err != nil {
				return "", nil, err
			}
			line += val

			setups = append(setups, stps...)
		} else {
			line = fmt.Sprintf("call $%s(", fsig.Name)
			for i, arg := range stmtNode.Children {
				argType := mapTypeToIRType(fsig.ArgTypes[i].R)
				val, stps, err := cg.GenerateExprIR(arg)
				if err != nil {
					return "", nil, err
				}
				line += fmt.Sprintf("%s %s", argType, val)
				if i != len(stmtNode.Children)-1 {
					line += ", "
				}

				setups = append(setups, stps...)
			}
		}
		line += ")"

	case parser.NODE_TYPE_IF:
		ifVal := stmtNode.Value.(*parser.IfNodeValue)
		endLabel := cg.GetTmpLbl()

		condVal, condSetups, err := cg.GenerateExprIR(ifVal.IfBranch.Condition)
		if err != nil {
			return "", nil, err
		}
		ifLabel := cg.GetTmpLbl()
		elseCheckLabel := cg.GetTmpLbl()

		setups = append(setups, condSetups...)
		setups = append(setups, fmt.Sprintf("jnz %s, %s, %s", condVal, ifLabel, elseCheckLabel))

		ifBlockIR, ifTerminated, err := cg.GenerateBlockIR(ifVal.IfBranch.Node, loopBegin, loopEnd)
		if err != nil {
			return "", nil, err
		}
		setups = append(setups, fmt.Sprintf("%s", ifLabel))
		setups = append(setups, ifBlockIR)
		if !ifTerminated {
			setups = append(setups, fmt.Sprintf("jmp %s", endLabel))
		}

		currentCheckLabel := elseCheckLabel

		for _, elseIf := range ifVal.ElseIfBranches {
			elseIfCondVal, elseIfCondSetups, err := cg.GenerateExprIR(elseIf.Condition)
			if err != nil {
				return "", nil, err
			}

			elseIfLabel := cg.GetTmpLbl()
			nextCheckLabel := cg.GetTmpLbl()

			setups = append(setups, fmt.Sprintf("%s", currentCheckLabel))
			setups = append(setups, elseIfCondSetups...)
			setups = append(setups, fmt.Sprintf("jnz %s, %s, %s", elseIfCondVal, elseIfLabel, nextCheckLabel))

			elseIfBlockIR, elseIfTerminated, err := cg.GenerateBlockIR(elseIf.Node, loopBegin, loopEnd)
			if err != nil {
				return "", nil, err
			}
			setups = append(setups, fmt.Sprintf("%s", elseIfLabel))
			setups = append(setups, elseIfBlockIR)
			if !elseIfTerminated {
				setups = append(setups, fmt.Sprintf("jmp %s", endLabel))
			}

			currentCheckLabel = nextCheckLabel
		}

		if ifVal.ElseBranch != nil {
			elseLabel := cg.GetTmpLbl()
			setups = append(setups, fmt.Sprintf("%s", currentCheckLabel))
			setups = append(setups, fmt.Sprintf("jmp %s", elseLabel))

			elseBlockIR, elseTerminated, err := cg.GenerateBlockIR(ifVal.ElseBranch.Node, loopBegin, loopEnd)
			if err != nil {
				return "", nil, err
			}
			setups = append(setups, fmt.Sprintf("%s", elseLabel))
			setups = append(setups, elseBlockIR)
			if !elseTerminated {
				setups = append(setups, fmt.Sprintf("jmp %s", endLabel))
			}
		} else {
			setups = append(setups, fmt.Sprintf("%s", currentCheckLabel))
			setups = append(setups, fmt.Sprintf("jmp %s", endLabel))
		}

		setups = append(setups, fmt.Sprintf("%s", endLabel))

	case parser.NODE_TYPE_FOR:
		value := stmtNode.Value.(*parser.ForLoopValue)
		beginLbl := cg.GetTmpLbl()
		bodyLbl := cg.GetTmpLbl()
		endLbl := cg.GetTmpLbl()
		line = beginLbl + "\n"
		body, _, err := cg.GenerateBlockIR(value.Body, beginLbl, endLbl)
		if err != nil {
			return "", nil, err
		}

		if len(value.ExprsOrStmts) == 1 {
			cond, stps, err := cg.GenerateExprIR(value.ExprsOrStmts[0])
			if err != nil {
				return "", nil, err
			}

			for _, stp := range stps {
				line += stp + "\n"
			}
			line += fmt.Sprintf("jnz %s %s, %s\n", cond, bodyLbl, endLbl)
			line += bodyLbl + "\n" + body + "\n"
			line += fmt.Sprintf("jmp %s\n", beginLbl)
		} else if len(value.ExprsOrStmts) == 3 {
			l, stps, err := cg.GenerateStmtIR(value.ExprsOrStmts[0], last, beginLbl, endLbl)
			if err != nil {
				return "", nil, err
			}
			setups = append(setups, l)

			setups = append(setups, stps...)

			cond, stps, err := cg.GenerateExprIR(value.ExprsOrStmts[1])
			if err != nil {
				return "", nil, err
			}

			for _, stp := range stps {
				line += stp + "\n"
			}
			line += fmt.Sprintf("jnz %s, %s, %s\n", cond, bodyLbl, endLbl)
			line += bodyLbl + "\n" + body + "\n"

			reass, stps, err := cg.GenerateStmtIR(value.ExprsOrStmts[2], last, beginLbl, endLbl)
			if err != nil {
				return "", nil, err
			}

			for _, stp := range stps {
				line += stp + "\n"
			}
			line += reass + "\n"
			line += fmt.Sprintf("jmp %s\n", beginLbl)
		} else {
			line += bodyLbl + "\n" + body + "\n"
		}

		line += endLbl
	default:
		return "", nil, shared.NewError(stmtNode.Loc, "unsupported statement %s", stmtNode.Type)
	}

	return line, setups, nil
}

func (cg *CodeGen) GenerateExprIR(exprNode *Node) (string, []string, error) {
	val := ""
	setups := []string{}
	switch exprNode.Type {

	case parser.NODE_TYPE_IDENTIFIER:
		val = fmt.Sprintf("%%%s", exprNode.Value.(string))

	case parser.NODE_TYPE_NUMBER_LITERAL:
		val = exprNode.Value.(string)

	case parser.NODE_TYPE_BOOL_LITERAL:
		if exprNode.Value.(string) == "true" {
			val = "1"
		} else {
			val = "0"
		}

	case parser.NODE_TYPE_FUNCTION_CALL:
		fname := typechecker.IdentToStr(exprNode.Value.(*Node))
		fsig := cg.funcSigs[fname]
		nm := cg.GetTmpVar()
		val = fmt.Sprintf("%s", nm)
		setup := fmt.Sprintf("%s =%s call $%s(", val, mapTypeToIRType(fsig.RetType), fname)
		for i, arg := range exprNode.Children {
			argType := mapTypeToIRType(fsig.ArgTypes[i].R)
			v, st, err := cg.GenerateExprIR(arg)
			if err != nil {
				return "", nil, err
			}

			setup += fmt.Sprintf("%s %s", argType, v)
			if i != len(exprNode.Children)-1 {
				setup += ", "
			}

			setups = append(setups, st...)
		}
		setup += ")"
		setups = append(setups, setup)

	case parser.NODE_TYPE_BINARY_OP:
		exprType := mapTypeToIRType(exprNode.Left.ExprType)

		op := ""
		switch exprNode.Value.(string) {
		case "+":
			op = "add"
		case "-":
			op = "sub"
		case "*":
			op = "mul"
		case "/":
			if shared.IsUnsignedType(exprNode.Left.ExprType) {
				op = "udiv"
			} else {
				op = "div"
			}
		case "%":
			if shared.IsUnsignedType(exprNode.Left.ExprType) {
				op = "urem"
			} else {
				op = "rem"
			}
		case "==":
			op = "ceq" + exprType
		case "!=":
			op = "cne" + exprType
		case "<":
			if shared.IsUnsignedType(exprNode.Left.ExprType) {
				op = "cult" + exprType
			} else {
				op = "cslt" + exprType
			}
		case ">":
			if shared.IsUnsignedType(exprNode.Left.ExprType) {
				op = "cugt" + exprType
			} else {
				op = "csgt" + exprType
			}
		case "<=":
			if shared.IsUnsignedType(exprNode.Left.ExprType) {
				op = "cule" + exprType
			} else {
				op = "csle" + exprType
			}
		case ">=":
			if shared.IsUnsignedType(exprNode.Left.ExprType) {
				op = "cuge" + exprType
			} else {
				op = "csge" + exprType
			}
		}

		nm := cg.GetTmpVar()
		val = fmt.Sprintf("%s", nm)
		v1, st1, err := cg.GenerateExprIR(exprNode.Left)
		if err != nil {
			return "", nil, err
		}
		v2, st2, err := cg.GenerateExprIR(exprNode.Right)
		if err != nil {
			return "", nil, err
		}
		setup := fmt.Sprintf("%s =%s %s %s, %s", val, exprType, op, v1, v2)
		setups = append(setups, st1...)
		setups = append(setups, st2...)
		setups = append(setups, setup)

	case parser.NODE_TYPE_UNARY_OP:
		nm := cg.GetTmpVar()
		val = fmt.Sprintf("%s", nm)
		setup := ""
		v, st, err := cg.GenerateExprIR(exprNode.Right)
		if err != nil {
			return "", nil, err
		}
		switch exprNode.Value.(string) {
		case "-":
			setup = fmt.Sprintf("%s =%s neg %s", val, mapTypeToIRType(exprNode.Right.ExprType), v)
		case "not":
			exprType := mapTypeToIRType(exprNode.Right.ExprType)
			setup = fmt.Sprintf("%s =%s cne%s %s, 1", val, exprType, exprType, v)

		}

		setups = append(setups, st...)
		setups = append(setups, setup)

	case parser.NODE_TYPE_IF_EXPR:
		ifEVal := exprNode.Value.(*parser.IfNodeValue)
		endLabel := cg.GetTmpLbl()
		nm := cg.GetTmpVar()
		val = fmt.Sprintf("%s", nm)
		exprType := mapTypeToIRType(exprNode.ExprType)

		condVal, condSetups, err := cg.GenerateExprIR(ifEVal.IfBranch.Condition)
		if err != nil {
			return "", nil, err
		}

		ifLabel := cg.GetTmpLbl()
		elseCheckLabel := cg.GetTmpLbl()

		setups = append(setups, condSetups...)
		setups = append(setups, fmt.Sprintf("jnz %s, %s, %s", condVal, ifLabel, elseCheckLabel))

		ifVal, ifSetups, err := cg.GenerateExprIR(ifEVal.IfBranch.Node)
		if err != nil {
			return "", nil, err
		}
		ifBlockCode := fmt.Sprintf("%s", ifLabel)
		for _, s := range ifSetups {
			ifBlockCode += "\n" + s
		}
		ifBlockCode += fmt.Sprintf("\n%s =%s copy %s", nm, exprType, ifVal)
		ifBlockCode += fmt.Sprintf("\njmp %s", endLabel)
		setups = append(setups, ifBlockCode)

		currentCheckLabel := elseCheckLabel

		for _, elseIf := range ifEVal.ElseIfBranches {
			elseIfCondVal, elseIfCondSetups, err := cg.GenerateExprIR(elseIf.Condition)
			if err != nil {
				return "", nil, err
			}

			elseIfLabel := cg.GetTmpLbl()
			nextCheckLabel := cg.GetTmpLbl()

			setups = append(setups, fmt.Sprintf("%s", currentCheckLabel))
			setups = append(setups, elseIfCondSetups...)
			setups = append(setups, fmt.Sprintf("jnz %s, %s, %s", elseIfCondVal, elseIfLabel, nextCheckLabel))

			elseIfVal, elseIfSetups, err := cg.GenerateExprIR(elseIf.Node)
			if err != nil {
				return "", nil, err
			}

			elseIfBlockCode := fmt.Sprintf("%s", elseIfLabel)
			for _, s := range elseIfSetups {
				elseIfBlockCode += "\n" + s
			}
			elseIfBlockCode += fmt.Sprintf("\n%s =%s copy %s", nm, exprType, elseIfVal)
			elseIfBlockCode += fmt.Sprintf("\njmp %s", endLabel)
			setups = append(setups, elseIfBlockCode)

			currentCheckLabel = nextCheckLabel
		}

		elseLabel := cg.GetTmpLbl()
		setups = append(setups, fmt.Sprintf("%s", currentCheckLabel))
		setups = append(setups, fmt.Sprintf("jmp %s", elseLabel))

		elseVal, elseSetups, err := cg.GenerateExprIR(ifEVal.ElseBranch.Node)
		if err != nil {
			return "", nil, err
		}
		elseBlockCode := fmt.Sprintf("%s", elseLabel)
		for _, s := range elseSetups {
			elseBlockCode += "\n" + s
		}
		elseBlockCode += fmt.Sprintf("\n%s =%s copy %s", nm, exprType, elseVal)
		elseBlockCode += fmt.Sprintf("\njmp %s", endLabel)
		setups = append(setups, elseBlockCode)

		setups = append(setups, fmt.Sprintf("%s", endLabel))

	case parser.NODE_TYPE_CAST:
		targetTypeIdent := exprNode.Left.Value.(string)
		targetType, _ := shared.ResolveType(targetTypeIdent)
		targetIRType := mapTypeToIRType(targetType)
		sourceType := exprNode.Right.ExprType
		sourceIRType := mapTypeToIRType(sourceType)

		sourceVal, sourceSetups, err := cg.GenerateExprIR(exprNode.Right)
		if err != nil {
			return "", nil, err
		}
		setups = append(setups, sourceSetups...)

		currentVal := sourceVal

		if sourceIRType != targetIRType {
			tmpVar := cg.GetTmpVar()

			if sourceIRType == "w" && targetIRType == "l" {
				if shared.IsUnsignedType(sourceType) {
					setups = append(setups, fmt.Sprintf("%s =l extuw %s", tmpVar, currentVal))
				} else {
					setups = append(setups, fmt.Sprintf("%s =l extsw %s", tmpVar, currentVal))
				}
				currentVal = tmpVar
			} else if sourceIRType == "l" && targetIRType == "w" {
				setups = append(setups, fmt.Sprintf("%s =w copy %s", tmpVar, currentVal))
				currentVal = tmpVar
			} else {
				return "", nil, shared.NewError(exprNode.Loc, "unsupported cast from %s to %s", sourceIRType, targetIRType)
			}
		}

		mask, ext := getTruncateMaskAndExt(targetType)
		if mask != 0 {
			tmpTrunc := cg.GetTmpVar()
			setups = append(setups, fmt.Sprintf("%s =w and %s, %d", tmpTrunc, currentVal, mask))
			currentVal = tmpTrunc

			if ext != "" {
				tmpExt := cg.GetTmpVar()
				setups = append(setups, fmt.Sprintf("%s =w %s %s", tmpExt, ext, currentVal))
				currentVal = tmpExt
			}
		}

		val = currentVal

	default:
		return "", nil, shared.NewError(exprNode.Loc, "unsupported expression %s", exprNode.Type)
	}

	mask, ext := getTruncateMaskAndExt(exprNode.ExprType)
	if mask != 0 {
		truncTmp := cg.GetTmpVar()
		setups = append(setups, fmt.Sprintf("%s =w and %s, %d", truncTmp, val, mask))
		val = truncTmp
		if ext != "" {
			extTmp := cg.GetTmpVar()
			setups = append(setups, fmt.Sprintf("%s =w %s %s", extTmp, ext, val))
			val = extTmp
		}
	}

	return val, setups, nil
}

func mapTypeToIRType(t Type) string {
	switch t {
	case shared.BUILTIN_VOID:
		return ""
	case shared.BUILTIN_BOOL:
		return "w"
	case shared.BUILTIN_I8, shared.BUILTIN_U8:
		return "w"
	case shared.BUILTIN_I16, shared.BUILTIN_U16:
		return "w"
	case shared.BUILTIN_I32, shared.BUILTIN_U32:
		return "w"
	case shared.BUILTIN_I64, shared.BUILTIN_U64:
		return "l"
	case shared.BUILTIN_UNTYPED_INT:
		return "l" // just in case
	default:
		panic("unknown type")
	}
}

func getTruncateMaskAndExt(t Type) (mask int, extOp string) {
	switch t {
	case shared.BUILTIN_U8:
		return 0xFF, ""
	case shared.BUILTIN_I8:
		return 0xFF, "extsb"
	case shared.BUILTIN_U16:
		return 0xFFFF, ""
	case shared.BUILTIN_I16:
		return 0xFFFF, "extsh"
	default:
		return 0, ""
	}
}