// SPDX-License-Identifier: Apache-2.0

package firrtl.passes

import firrtl._
import firrtl.ir._
import firrtl.Utils._
import firrtl.traversals.Foreachers._
import firrtl.options.Dependency

object CheckFlows extends Pass {

  override def prerequisites = Dependency(passes.ResolveFlows) +: firrtl.stage.Forms.MinimalHighForm

  override def optionalPrerequisiteOf =
    Seq(
      Dependency[passes.InferBinaryPoints],
      Dependency[passes.TrimIntervals],
      Dependency[passes.InferWidths],
      Dependency[transforms.InferResets]
    )

  override def invalidates(a: Transform) = false

  type FlowMap = collection.mutable.HashMap[String, Flow]

  implicit def toStr(g: Flow): String = g match {
    case SourceFlow  => "source"
    case SinkFlow    => "sink"
    case UnknownFlow => "unknown"
    case DuplexFlow  => "duplex"
  }

  class WrongFlow(info: Info, mname: String, expr: String, wrong: Flow, right: Flow)
      extends PassException(
        s"$info: [module $mname]  Expression $expr is used as a $wrong but can only be used as a $right."
      )

  def run(c: Circuit): Circuit = {
    val errors = new Errors()

    def get_flow(e: Expression, flows: FlowMap): Flow = e match {
      case (e: WRef)      => flows(e.name)
      case (e: WSubIndex) => get_flow(e.expr, flows)
      case (e: WSubAccess) => get_flow(e.expr, flows)
      case (e: WSubField) =>
        e.expr.tpe match {
          case t: BundleType =>
            val f = (t.fields.find(_.name == e.name)).get
            times(get_flow(e.expr, flows), f.flip)
        }
      case _ => SourceFlow
    }

    def flip_q(t: Type): Boolean = {
      def flip_rec(t: Type, f: Orientation): Boolean = t match {
        case tx: BundleType => tx.fields.exists(field => flip_rec(field.tpe, times(f, field.flip)))
        case tx: VectorType => flip_rec(tx.tpe, f)
        case tx => f == Flip
      }
      flip_rec(t, Default)
    }

    def check_flow(info: Info, mname: String, flows: FlowMap, desired: Flow)(e: Expression): Unit = {
      val flow = get_flow(e, flows)
      (flow, desired) match {
        case (SourceFlow, SinkFlow) =>
          errors.append(new WrongFlow(info, mname, e.serialize, desired, flow))
        case (SinkFlow, SourceFlow) =>
          kind(e) match {
            case PortKind | InstanceKind if !flip_q(e.tpe) => // OK!
            case _ =>
              errors.append(new WrongFlow(info, mname, e.serialize, desired, flow))
          }
        case _ =>
      }
    }

    def check_flows_e(info: Info, mname: String, flows: FlowMap)(e: Expression): Unit = {
      e match {
        case e: Mux    => e.foreach(check_flow(info, mname, flows, SourceFlow))
        case e: DoPrim => e.args.foreach(check_flow(info, mname, flows, SourceFlow))
        case _ =>
      }
      e.foreach(check_flows_e(info, mname, flows))
    }

    def check_flows_s(minfo: Info, mname: String, flows: FlowMap)(s: Statement): Unit = {
      val info = get_info(s) match {
        case NoInfo => minfo
        case x      => x
      }
      s match {
        case (s: DefWire)     => flows(s.name) = DuplexFlow
        case (s: DefRegister) => flows(s.name) = DuplexFlow
        case (s: DefMemory)   => flows(s.name) = SourceFlow
        case (s: WDefInstance) => flows(s.name) = SourceFlow
        case (s: DefNode) =>
          check_flow(info, mname, flows, SourceFlow)(s.value)
          flows(s.name) = SourceFlow
        case (s: Connect) =>
          check_flow(info, mname, flows, SinkFlow)(s.loc)
          check_flow(info, mname, flows, SourceFlow)(s.expr)
        case (s: Print) =>
          s.args.foreach(check_flow(info, mname, flows, SourceFlow))
          check_flow(info, mname, flows, SourceFlow)(s.en)
          check_flow(info, mname, flows, SourceFlow)(s.clk)
        case (s: PartialConnect) =>
          check_flow(info, mname, flows, SinkFlow)(s.loc)
          check_flow(info, mname, flows, SourceFlow)(s.expr)
        case (s: Conditionally) =>
          check_flow(info, mname, flows, SourceFlow)(s.pred)
        case (s: Stop) =>
          check_flow(info, mname, flows, SourceFlow)(s.en)
          check_flow(info, mname, flows, SourceFlow)(s.clk)
        case (s: Verification) =>
          check_flow(info, mname, flows, SourceFlow)(s.clk)
          check_flow(info, mname, flows, SourceFlow)(s.pred)
          check_flow(info, mname, flows, SourceFlow)(s.en)
        case _ =>
      }
      s.foreach(check_flows_e(info, mname, flows))
      s.foreach(check_flows_s(minfo, mname, flows))
    }

    for (m <- c.modules) {
      val flows = new FlowMap
      flows ++= (m.ports.map(p => p.name -> to_flow(p.direction)))
      m.foreach(check_flows_s(m.info, m.name, flows))
    }
    errors.trigger()
    c
  }
}
