/*                                                                           *\
**        _____ ____  _____   _____    __                                    **
**       / ___// __ \/  _/ | / /   |  / /   HDL Core                         **
**       \__ \/ /_/ // //  |/ / /| | / /    (c) Dolu, All rights reserved    **
**      ___/ / ____// // /|  / ___ |/ /___                                   **
**     /____/_/   /___/_/ |_/_/  |_/_____/                                   **
**                                                                           **
**      This library is free software; you can redistribute it and/or        **
**    modify it under the terms of the GNU Lesser General Public             **
**    License as published by the Free Software Foundation; either           **
**    version 3.0 of the License, or (at your option) any later version.     **
**                                                                           **
**      This library is distributed in the hope that it will be useful,      **
**    but WITHOUT ANY WARRANTY; without even the implied warranty of         **
**    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU      **
**    Lesser General Public License for more details.                        **
**                                                                           **
**      You should have received a copy of the GNU Lesser General Public     **
**    License along with this library.                                       **
\*                                                                           */
package spinal.core

import spinal.core.internals._
import spinal.idslplugin.Location

import scala.collection.mutable.ArrayBuffer


trait ConditionalContext extends GlobalDataUser {
}


class SwitchContext(val statement: SwitchStatement) {
}


object ConditionalContext {

  def isTrue(rootScope: ScopeStatement): Bool ={
    val globalData = GlobalData.get

    if(DslScopeStack.get == rootScope) return True

    val ctx = DslScopeStack.set(rootScope)

    val swap = rootScope.swap()
    val cond = False
    swap.appendBack()

    ctx.restore()

    cond := True
    cond
  }

  def isTrue(): Bool = isTrue(DslScopeStack.get.component.dslBody)
}


/**
  * Allow to express hardware conditional statements based on a `Bool` expression like an `if`.
  *
  * @example {{{
  *     when(cond1) {
  *       myCnt := 0
  *     }elsewhen(cond2) {
  *       myCnt := myCnt + 1
  *     }otherwise {
  *       myCnt := myCnt - 1
  *     }
  * }}}
  *
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Semantic/when_switch.html#when `when` Documentation]]
  */
object when {

//  def apply(cond: Bool)(block: => Unit)(implicit line: sourcecode.Line, file: sourcecode.File): WhenContext = {
  def apply(cond: Bool)(block: => Unit)(implicit loc: Location): WhenContext = {
    if(cond.globalData.config.nameWhenByFile ) {
      if (cond.dlcIsEmpty || !cond.head.source.isInstanceOf[Operator.Formal.InitState]) {
        cond.setName("when_" + loc.fileSymbol + "_l" + loc.line, Nameable.REMOVABLE)
      }
    }
    val whenStatement = new WhenStatement(cond)
    val whenContext   = new WhenContext(whenStatement)

    DslScopeStack.get.append(whenStatement)

    val ctx = DslScopeStack.set(whenStatement.whenTrue)
    block
    ctx.restore()

    whenContext
  }
}

class ElseWhenClause(val cond : Bool, _block: => Unit){
  def unary_! : ElseWhenClause = new ElseWhenClause(!cond, _block)
  def block = _block
}


/** Used by SpinalHDL `when`/`elsewhen`/`otherwise` and `WhenBuilder` to keep track of the structure.
  */
class WhenContext(whenStatement: WhenStatement) extends ConditionalContext with ScalaLocated {

  def otherwise(block: => Unit): Unit = {
    val ctx = whenStatement.whenFalse.push()
    block
    ctx.restore()
  }

  /** Allows to express conditional SpinalHDL statements in a `when` structure.
   * @see  [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Semantic/when_switch.html#when `when` Documentation]]
   */
  def elsewhen(clause : ElseWhenClause)(implicit loc: Location) : WhenContext = protElsewhen(clause.cond)(clause.block)(loc)

  // @deprecated("Use `elsewhen` instead of `.elsewhen` (without the prefix `.`)", "1.1.2")
  /** Allows to express conditional SpinalHDL statements in a `when` structure.
   * @see  [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Semantic/when_switch.html#when `when` Documentation]]
   */
  def elsewhen(cond: Bool)(block: => Unit)(implicit loc: Location): WhenContext = protElsewhen(cond)(block)(loc)

  protected def protElsewhen(cond: Bool)(block: => Unit)(loc: Location): WhenContext = {
    var newWhenContext: WhenContext = null
    otherwise {
      newWhenContext = when(cond)(block)(loc)
    }
    newWhenContext
  }
}

/** `case`/`switch` SpinalHDL statement
  *
  * @example {{{
  *     switch(x) {
  *         is(value1) {
  *             // execute when x === value1
  *         }
  *         is(value2) {
  *             // execute when x === value2
  *         }
  *         default {
  *            // execute if none of precedent conditions are meet
  *         }
  *      }
  * }}}
  *
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Semantic/when_switch.html#switch `switch` Documentation]]
  */
object switch {

    def apply[T <: BaseType](value: T, coverUnreachable : Boolean = false, strict : Boolean = true)(block: => Unit)(implicit loc : Location): Unit = {
      if(value.globalData.config.nameWhenByFile) {
        value.setName("switch_" + loc.fileSymbol + "_l" + loc.line, Nameable.REMOVABLE)
      }
      val globalData      = value.globalData
      val switchStatement = new SwitchStatement(value)
      val switchContext   = new SwitchContext(switchStatement)

      switchStatement.removeDuplication = !strict

      switchStatement.coverUnreachable = coverUnreachable
      SwitchStack(switchContext).on(block)
      DslScopeStack.get.append(switchStatement)
  }
}


/** `is` statement of a SpinalHDL `switch`/ `case`
  *
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Semantic/when_switch.html#switch `switch` Documentation]]  
  */
object is {

  def apply(value: Any, values:  Any*)(block: => Unit): Unit = list((value +: values).iterator)(block)

  def list(values: Iterator[Any])(block: => Unit): Unit = {

    val globalData    = GlobalData.get
    val switchContext = SwitchStack.get
    val switchElement = new SwitchStatementElement(ArrayBuffer[Expression](), new ScopeStatement(switchContext.statement))
    val switchValue   = switchContext.statement.value

    def onBaseType(value : BaseType): Unit = {
      if(value.getClass == switchValue.getClass){
        switchElement.keys += value
      }else{
        SpinalError(s"is(xxx) doesn't match switch(yyy) type (${value.getClass.getName} != ${switchValue.getClass.getName})")
      }
    }

    values.foreach {
      case value: BaseType => onBaseType(value)
      case key : Boolean   =>
        switchValue match {
          case switchValue: Bool => onBaseType(Bool(key))
          case _                 => SpinalError("The switch is not a Bool")
        }
      case key: Int        =>
        switchValue match {
          case switchValue: Bits => onBaseType(B(key))
          case switchValue: UInt => onBaseType(U(key))
          case switchValue: SInt => onBaseType(S(key))
          case _                 => SpinalError(s"The switch is not a Bits, UInt or SInt (${switchValue.getClass().getName()})")
        }
      case key: Long       =>
        switchValue match {
          case switchValue: Bits => onBaseType(B(key))
          case switchValue: UInt => onBaseType(U(key))
          case switchValue: SInt => onBaseType(S(key))
          case _                 => SpinalError("The switch is not a Bits, UInt or SInt")
        }
      case key: BigInt     =>
        switchValue match {
          case switchValue: Bits => onBaseType(B(key))
          case switchValue: UInt => onBaseType(U(key))
          case switchValue: SInt => onBaseType(S(key))
          case _                 => SpinalError("The switch is not a Bits, UInt or SInt")
        }
      case value: SpinalEnumElement[_] => onBaseType(value())
      case key: MaskedLiteral          => switchValue match {
        case switchValue: Bits => switchElement.keys += SwitchStatementKeyBool(switchValue === key, key)
        case switchValue: UInt => switchElement.keys += SwitchStatementKeyBool(switchValue === key, key)
        case switchValue: SInt => switchElement.keys += SwitchStatementKeyBool(switchValue === key, key)
        case _                 => SpinalError("The switch is not a Bits, UInt or SInt")
      }
      //    }
      //              case key: Data => switchValue.isEquals(key)
//      case key: Seq[Data] => switchElement.keys += SwitchStatementKeyBool(key.map(d => (switchValue.asInstanceOf[Data] === d)).reduce(_ || _))
    }

    switchContext.statement.elements += switchElement
    val ctx = switchElement.scopeStatement.push()
    block
    ctx.restore()
  }
}


/** `default` statement of a SpinalHDL `switch`/ `case`
  *
  * @see [[https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Semantic/when_switch.html#switch `switch` Documentation]]  
  */
object default {

  def apply(block: => Unit): Unit = {

    val switchContext = SwitchStack.get
    val defaultScope  =  new ScopeStatement(switchContext.statement)

    switchContext.statement.defaultScope = defaultScope

    val ctx = defaultScope.push()
    block
    ctx.restore()
  }
}
