package wacc

object assemblyIR {

  sealed trait AsmLine
  sealed trait Operand
  sealed trait Src extends Operand // mem location, register and imm value
  sealed trait Dest extends Operand // mem location and register

  enum Size {
    case Q64, D32, W16, B8

    def toInt: Int = this match {
      case Q64 => 8
      case D32 => 4
      case W16 => 2
      case B8  => 1
    }

    private val ptr = "ptr "

    override def toString(): String = this match {
      case Q64 => "qword " + ptr
      case D32 => "dword " + ptr
      case W16 => "word " + ptr
      case B8  => "byte " + ptr
    }
  }

  enum RegName {
    case AX, BX, CX, DX, SI, DI, SP, BP, IP, R8, R9, R10, R11, R12, R13, R14, R15
  }

  case class Register(size: Size, name: RegName) extends Dest with Src {
    import RegName._

    if (size == Size.B8 && name == RegName.IP) {
      throw new IllegalArgumentException("Cannot have 8 bit register for IP")
    }
    override def toString = name match {
      case AX  => tradToString("ax", "al")
      case BX  => tradToString("bx", "bl")
      case CX  => tradToString("cx", "cl")
      case DX  => tradToString("dx", "dl")
      case SI  => tradToString("si", "sil")
      case DI  => tradToString("di", "dil")
      case SP  => tradToString("sp", "spl")
      case BP  => tradToString("bp", "bpl")
      case IP  => tradToString("ip", "#INVALID")
      case R8  => newToString(8)
      case R9  => newToString(9)
      case R10 => newToString(10)
      case R11 => newToString(11)
      case R12 => newToString(12)
      case R13 => newToString(13)
      case R14 => newToString(14)
      case R15 => newToString(15)
    }

    private def tradToString(base: String, byteName: String): String =
      size match {
        case Size.Q64 => "r" + base
        case Size.D32 => "e" + base
        case Size.W16 => base
        case Size.B8  => byteName
      }

    private def newToString(base: Int): String = {
      val b = base.toString
      "r" + (size match {
        case Size.Q64 => b
        case Size.D32 => b + "d"
        case Size.W16 => b + "w"
        case Size.B8  => b + "b"
      })
    }
  }

  // arguments
  enum CLibFunc extends Operand {
    case Scanf,
      Fflush,
      Exit,
      PrintF,
      Malloc,
      Free

    private val plt = "@plt"

    override def toString = this match {
      case Scanf  => "scanf" + plt
      case Fflush => "fflush" + plt
      case Exit   => "exit" + plt
      case PrintF => "printf" + plt
      case Malloc => "malloc" + plt
      case Free   => "free" + plt
    }
  }

  case class MemLocation(
      base: Register,
      offset: Int | LabelArg = 0,
      // scale 0 will make register irrelevant, no other reason as to why it's RAX
      scaledIndex: (Register, Int) = (Register(Size.Q64, RegName.AX), 0),
      opSize: Option[Size] = None
  ) extends Dest
      with Src {
    def copy(
        base: Register = this.base,
        offset: Int | LabelArg = this.offset,
        scaledIndex: (Register, Int) = this.scaledIndex,
        opSize: Option[Size] = this.opSize
    ): MemLocation = MemLocation(base, offset, scaledIndex, opSize)

    override def toString(): String = {
      val opSizeStr = opSize.map(_.toString).getOrElse("")
      val baseStr = base.toString
      val offsetStr = offset match {
        case 0   => ""
        case off => s" + $off"
      }
      val scaledIndexStr = scaledIndex match {
        case (reg, scale) if scale != 0 => s" + $reg * $scale"
        case _                          => ""
      }
      s"$opSizeStr[$baseStr$scaledIndexStr$offsetStr]"
    }
  }

  case class ImmediateVal(value: Int) extends Src {
    override def toString = value.toString
  }

  case class LabelArg(name: String) extends Operand {
    override def toString = name
  }

  abstract class Operation(ins: String, ops: Operand*) extends AsmLine {
    override def toString: String = s"\t$ins ${ops.mkString(", ")}"
  }

  // arithmetic operations
  case class Add(op1: Dest, op2: Src) extends Operation("add", op1, op2)
  case class Subtract(op1: Dest, op2: Src) extends Operation("sub", op1, op2)
  case class Multiply(ops: Operand*) extends Operation("imul", ops*)
  case class Divide(op1: Src) extends Operation("idiv", op1)
  case class Negate(op: Dest) extends Operation("neg", op)
  // bitwise operations
  case class And(op1: Dest, op2: Src) extends Operation("and", op1, op2)
  case class Or(op1: Dest, op2: Src) extends Operation("or", op1, op2)
  case class Xor(op1: Dest, op2: Src) extends Operation("xor", op1, op2)
  case class Compare(op1: Dest, op2: Src) extends Operation("cmp", op1, op2)
  case class CDQ() extends Operation("cdq")
  // stack operations
  case class Push(op1: Src) extends Operation("push", op1)
  case class Pop(op1: Src) extends Operation("pop", op1)
  // move operations
  case class Move(op1: Dest, op2: Src) extends Operation("mov", op1, op2)
  case class Load(op1: Register, op2: MemLocation) extends Operation("lea ", op1, op2)

  // function call operations
  case class Call(op1: CLibFunc | LabelArg) extends Operation("call", op1)
  case class Return() extends Operation("ret")

  // conditional operations
  case class Jump(op1: LabelArg, condition: Cond = Cond.Always)
      extends Operation(s"j${condition.toString}", op1)
  case class Set(op1: Dest, condition: Cond = Cond.Always)
      extends Operation(s"set${condition.toString}", op1)

  case class LabelDef(name: String) extends AsmLine {
    override def toString = s"$name:"
  }

  case class Comment(comment: String) extends AsmLine {
    override def toString =
      comment.split("\n").map(line => s"# ${line}").mkString("\n")
  }

  enum Cond {
    case Equal,
      NotEqual,
      Greater,
      GreaterEqual,
      Less,
      LessEqual,
      Overflow,
      Always
    override def toString(): String = this match {
      case Equal        => "e"
      case NotEqual     => "ne"
      case Greater      => "g"
      case GreaterEqual => "ge"
      case Less         => "l"
      case LessEqual    => "le"
      case Overflow     => "o"
      case Always       => "mp"
    }
  }

  enum Directive extends AsmLine {
    case IntelSyntax, RoData, Text
    case Global(name: String)
    case Int(value: scala.Int)
    case Asciz(string: String)

    override def toString(): String = this match {
      case IntelSyntax   => ".intel_syntax noprefix"
      case Global(name)  => s".globl $name"
      case Text          => ".text"
      case RoData        => ".section .rodata"
      case Int(value)    => s"\t.int $value"
      case Asciz(string) => s"\t.asciz \"$string\""
    }
  }

  enum PrintFormat {
    case Int, Char, String

    override def toString(): String = this match {
      case Int    => "%d"
      case Char   => "%c"
      case String => "%s"
    }
  }

  object commonRegisters {
    import Size._
    import RegName._

    val RAX = Register(Q64, AX)
    val EAX = Register(D32, AX)
    val RDI = Register(Q64, DI)
    val RIP = Register(Q64, IP)
    val RBP = Register(Q64, BP)
    val RSI = Register(Q64, SI)
    val RDX = Register(Q64, DX)
    val RCX = Register(Q64, CX)
    val ECX = Register(D32, CX)
  }
}
