/*
 * Scala (https://www.scala-lang.org)
 *
 * Copyright EPFL and Lightbend, Inc.
 *
 * Licensed under Apache License 2.0
 * (http://www.apache.org/licenses/LICENSE-2.0).
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package scala.tools.nsc
package ast

import scala.annotation.unused
import scala.language.implicitConversions

/** A DSL for generating scala code.  The goal is that the
 *  code generating code should look a lot like the code it
 *  generates.
 */

trait TreeDSL {
  val global: Global

  import global._
  import definitions._

  object CODE {
    // Add a null check to a Tree => Tree function
    def nullSafe[T](f: Tree => Tree, ifNull: Tree): Tree => Tree =
      tree => IF (tree MEMBER_== NULL) THEN ifNull ELSE f(tree)

    object LIT extends (Any => Literal) {
      def typed(x: Any)   = apply(x) setType ConstantType(Constant(x))
      def apply(x: Any)   = Literal(Constant(x))
    }

    // Boring, predictable trees.
    def TRUE  = LIT typed true
    def FALSE = LIT typed false
    def ZERO  = LIT(0)
    def NULL  = LIT(null)
    def UNIT  = LIT(())

    def fn(lhs: Tree, op:   Name, args: Tree*)  = Apply(Select(lhs, op), args.toList)
    def fn(lhs: Tree, op: Symbol, args: Tree*)  = Apply(Select(lhs, op), args.toList)

    class TreeMethods(target: Tree) {
      /** logical/comparison ops **/
      def OR(other: Tree) =
        if (target == EmptyTree) other
        else if (other == EmptyTree) target
        else gen.mkOr(target, other)

      def AND(other: Tree) =
        if (target == EmptyTree) other
        else if (other == EmptyTree) target
        else gen.mkAnd(target, other)

      /** Note - calling ANY_== in the matcher caused primitives to get boxed
       *  for the comparison, whereas looking up nme.EQ does not.  See #3570 for
       *  an example of how target.tpe can be non-null, yet it claims not to have
       *  a member called nme.EQ.  Not sure if that should happen, but we can be
       *  robust by dragging in Any regardless.
       */
      def MEMBER_== (other: Tree)   = fn(target, (if (target.tpe == null) NoSymbol else target.tpe member nme.EQ).orElse(Any_==), other)
      def ANY_EQ  (other: Tree)     = OBJ_EQ(other AS ObjectTpe)
      def ANY_==  (other: Tree)     = fn(target, Any_==, other)
      def ANY_!=  (other: Tree)     = fn(target, Any_!=, other)
      def OBJ_EQ  (other: Tree)     = fn(target, Object_eq,     other)
      def OBJ_NE  (other: Tree)     = fn(target, Object_ne,     other)
      def OBJ_==  (other: Tree)     = fn(target, Object_equals, other)
      def OBJ_##                    = fn(target, Object_hashCode)

      def INT_>=  (other: Tree)     = fn(target, getMember(IntClass, nme.GE), other)
      def INT_==  (other: Tree)     = fn(target, getMember(IntClass, nme.EQ), other)
      def INT_-   (other: Tree)     = fn(target, getMember(IntClass, nme.MINUS), other)

      // generic operations on ByteClass, IntClass, LongClass
      @unused("avoid warning for multiple parameters")
      def GEN_|   (other: Tree, kind: ClassSymbol)  = fn(target, getMember(kind, nme.OR), other)
      @unused("avoid warning for multiple parameters")
      def GEN_&   (other: Tree, kind: ClassSymbol)  = fn(target, getMember(kind, nme.AND), other)
      @unused("avoid warning for multiple parameters")
      def GEN_==  (other: Tree, kind: ClassSymbol)  = fn(target, getMember(kind, nme.EQ), other)
      @unused("avoid warning for multiple parameters")
      def GEN_!=  (other: Tree, kind: ClassSymbol)  = fn(target, getMember(kind, nme.NE), other)

      /** Apply, Select, Match **/
      def APPLY(params: Tree*)      = Apply(target, params.toList)
      def APPLY(params: List[Tree]) = Apply(target, params)

      def DOT(member: Name)         = SelectStart(Select(target, member))
      def DOT(sym: Symbol)          = SelectStart(Select(target, sym))

      /** Assignment */
      // !!! This method is responsible for some tree sharing, but a diligent
      // reviewer pointed out that we shouldn't blindly duplicate these trees
      // as there might be DefTrees nested beneath them.  It's not entirely
      // clear how to proceed, so for now it retains the non-duplicating behavior.
      def ===(rhs: Tree)            = Assign(target, rhs)

      /** Casting & type tests -- working our way toward understanding exactly
       *  what differs between the different forms of IS and AS.
       *
       *  See ticket #2168 for one illustration of AS vs. AS_ANY.
       */
      def AS(tpe: Type)       = gen.mkAsInstanceOf(target, tpe, any = true, wrapInApply = false)
      def IS_OBJ(tpe: Type)   = gen.mkIsInstanceOf(target, tpe, any = false)

      def GETCLASS()          = fn(target, Object_getClass)
    }

    case class SelectStart(tree: Select) {
      def apply(args: Tree*) = Apply(tree, args.toList)
    }

    class CaseStart(pat: Tree, guard: Tree) {
      def IF(g: Tree): CaseStart    = new CaseStart(pat, g)
      def ==>(body: Tree): CaseDef  = CaseDef(pat, guard, body)
    }

    class IfStart(cond: Tree, thenp: Tree) {
      def THEN(x: Tree)     = new IfStart(cond, x)
      def ELSE(elsep: Tree) = If(cond, thenp, elsep)
      def ENDIF             = If(cond, thenp, EmptyTree)
    }
    class TryStart(body: Tree, catches: List[CaseDef], fin: Tree) {
      def CATCH(xs: CaseDef*) = new TryStart(body, xs.toList, fin)
      def FINALLY(end: END.type) = Try(body, catches, fin)
      def FINALLY(fin1: Tree) = Try(body, catches, fin1)
    }
    object END

    def CASE(pat: Tree): CaseStart  = new CaseStart(pat, EmptyTree)
    def DEFAULT: CaseStart          = new CaseStart(Ident(nme.WILDCARD), EmptyTree)

    def NEW(tpt: Tree, args: Tree*): Tree   = New(tpt, List(args.toList))

    def NOT(tree: Tree)   = Select(tree, Boolean_not)
    def AND(guards: Tree*) = {
      def binaryTreeAnd(tests: Seq[Tree]): Tree = tests match{
        case Seq() => EmptyTree
        case Seq(single) => single
        case multiple =>
          val (before, after) = multiple.splitAt(tests.size / 2)
          gen.mkAnd(binaryTreeAnd(before), binaryTreeAnd(after))
      }

      binaryTreeAnd(guards)
    }

    def IF(tree: Tree)    = new IfStart(tree, EmptyTree)
    def TRY(tree: Tree)   = new TryStart(tree, Nil, EmptyTree)
    def BLOCK(xs: Tree*)  = Block(xs.init.toList, xs.last)
    def SOME(xs: Tree*)   = Apply(SomeClass.companionSymbol, gen.mkTuple(xs.toList))

    /** Typed trees from symbols. */
    def REF(sym: Symbol): RefTree            = gen.mkAttributedRef(sym)
    def REF(pre: Type, sym: Symbol): RefTree = gen.mkAttributedRef(pre, sym)

    /** Implicits - some of these should probably disappear **/
    implicit def mkTreeMethods(target: Tree): TreeMethods = new TreeMethods(target)
    implicit def mkTreeMethodsFromSymbol(target: Symbol): TreeMethods = new TreeMethods(Ident(target))

    /** (foo DOT bar) might be simply a Select, but more likely it is to be immediately
     *  followed by an Apply.  We don't want to add an actual apply method to arbitrary
     *  trees, so SelectStart is created with an apply - and if apply is not the next
     *  thing called, the implicit from SelectStart -> Tree will provide the tree.
     */
    implicit def mkTreeFromSelectStart(ss: SelectStart): Select = ss.tree
    implicit def mkTreeMethodsFromSelectStart(ss: SelectStart): TreeMethods = mkTreeMethods(ss.tree)
  }
}
