package amyc
package interpreter

import utils._
import ast.SymbolicTreeModule._
import ast.Identifier
import analyzer.SymbolTable

// An interpreter for Amy programs, implemented in Scala
object Interpreter extends Pipeline[(Program, SymbolTable), Unit]:

  // A class that represents a value computed by interpreting an expression
  enum Value:
    def asInt: Int = this.asInstanceOf[IntValue].i
    def asBoolean: Boolean = this.asInstanceOf[BooleanValue].b
    def asString: String = this.asInstanceOf[StringValue].s

    override def toString: String = this match
      case IntValue(i) => i.toString
      case BooleanValue(b) => b.toString
      case StringValue(s) => s
      case UnitValue => "()"
      case CaseClassValue(constructor, args) =>
        constructor.name + "(" + args.map(_.toString).mkString(", ") + ")"

    case IntValue(i: Int)
    case BooleanValue(b: Boolean)
    case StringValue(s: String)
    case UnitValue
    case CaseClassValue(constructor: Identifier, args: List[Value])
  
  import Value._

  def run(ctx: Context)(v: (Program, SymbolTable)): Unit =
    val (program, table) = v

    // These built-in functions do not have an Amy implementation in the program,
    // instead their implementation is encoded in this map
    val builtIns: Map[(String, String), (List[Value]) => Value] = Map(
      ("Std", "printInt")    -> { args => println(args.head.asInt); UnitValue },
      ("Std", "printString") -> { args => println(args.head.asString); UnitValue },
      ("Std", "readString")  -> { args => StringValue(scala.io.StdIn.readLine()) },
      ("Std", "readInt")     -> { args =>
        val input = scala.io.StdIn.readLine()
        try
          IntValue(input.toInt)
        catch
          case ne: NumberFormatException =>
            ctx.reporter.fatal(s"""Could not parse "$input" to Int""")
      },
      ("Std", "intToString")   -> { args => StringValue(args.head.asInt.toString) },
      ("Std", "digitToString") -> { args => StringValue(args.head.asInt.toString) }
    )

    // Utility functions to interface with the symbol table.
    def isConstructor(name: Identifier) = table.getConstructor(name).isDefined
    def findFunctionOwner(functionName: Identifier) = table.getFunction(functionName).get.owner.name
    def findFunction(owner: String, name: String) =
      program.modules.find(_.name.name == owner).get.defs.collectFirst:
        case fd@FunDef(fn, _, _, _) if fn.name == name => fd
      .get

    // Interprets a function, using evaluations for local variables contained in 'locals'
    // TODO: Complete all missing cases. Look at the given ones for guidance. 
    def interpret(expr: Expr)(implicit locals: Map[Identifier, Value]): Value =
      expr match
        case Variable(name) =>
          ???
        case IntLiteral(i) =>
          ???
        case BooleanLiteral(b) =>
          ???
        case StringLiteral(s) =>
          ???
        case UnitLiteral() =>
          ???
        case Plus(lhs, rhs) =>
          IntValue(interpret(lhs).asInt + interpret(rhs).asInt)
        case Minus(lhs, rhs) =>
          ???
        case Times(lhs, rhs) =>
          ???
        case Div(lhs, rhs) =>
          ???
        case Mod(lhs, rhs) =>
          ???
        case LessThan(lhs, rhs) =>
          ???
        case LessEquals(lhs, rhs) =>
          ???
        case And(lhs, rhs) =>
          ???
        case Or(lhs, rhs) =>
          ???
        case Equals(lhs, rhs) =>
          ??? // Hint: Take care to implement Amy equality semantics
        case Concat(lhs, rhs) =>
          ???
        case Not(e) =>
          ???
        case Neg(e) =>
          ???
        case Call(qname, args) =>
          ???
          // Hint: Check if it is a call to a constructor first,
          //       then if it is a built-in function (otherwise it is a normal function).
          //       Use the helper methods provided above to retrieve information from the symbol table.
          //       Think how locals should be modified.
        case Sequence(e1, e2) =>
          ???
        case Let(df, value, body) =>
          ???
        case Ite(cond, thenn, elze) =>
          ???
        case Match(scrut, cases) =>
          ???
          // Hint: We give you a skeleton to implement pattern matching
          //       and the main body of the implementation
          val evS = interpret(scrut)

          // None = pattern does not match
          // Returns a list of pairs id -> value,
          // where id has been bound to value within the pattern.
          // Returns None when the pattern fails to match.
          // Note: Only works on well typed patterns (which have been ensured by the type checker).
          def matchesPattern(v: Value, pat: Pattern): Option[List[(Identifier, Value)]] =
            ((v, pat): @unchecked) match
              case (_, WildcardPattern()) => 
                  ???
              case (_, IdPattern(name)) => 
                  ???
              case (IntValue(i1), LiteralPattern(IntLiteral(i2))) =>
                  ???
              case (BooleanValue(b1), LiteralPattern(BooleanLiteral(b2))) =>
                  ???
              case (StringValue(_), LiteralPattern(StringLiteral(_))) =>
                  ???
              case (UnitValue, LiteralPattern(UnitLiteral())) =>
                  ???
              case (CaseClassValue(con1, realArgs), CaseClassPattern(con2, formalArgs)) =>
                  ???
          // Main "loop" of the implementation: Go through every case,
          // check if the pattern matches, and if so return the evaluation of the case expression
          cases.to(LazyList).map(matchCase => 
            val MatchCase(pat, rhs) = matchCase
            (rhs, matchesPattern(evS, pat))
          ).find(_._2.isDefined) match
            case Some((rhs, Some(moreLocals))) =>
              interpret(rhs)(using locals ++ moreLocals)
            case _ =>
              // No case matched
              ctx.reporter.fatal(s"Match error: ${evS.toString}@${scrut.position}")

        case Error(msg) =>
          ???
    end interpret

    for
      m <- program.modules
      e <- m.optExpr
    do
      interpret(e)(using Map())
    
  end run
