/*
 * Copyright 2017 Kailuo Wang
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package mainecoon

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.meta._
import autoProductNK._
import Util._

import collection.immutable.Seq

/**
 * auto generates methods in companion object to compose multiple interpreters into an interpreter of a `TupleNK` effects
 */
@compileTimeOnly("Cannot expand @autoProductK")
class autoProductNK extends StaticAnnotation {
  inline def apply(defn: Any): Any = meta {
    enrich(defn)(productKInst)
  }
}

object autoProductNK {
  private[mainecoon] def productKInst(cls: TypeDefinition): TypeDefinition = {
    import cls._

    def productMethod(arity: Int): Stat = {
      val range = (1 to arity).map(_.toString)

      // "F1, F2, F3"
      val effectTypeParamsNames = range.map(n => Type.Name("F" + n))

      //Tuple3K
      val productTypeName = Type.Name(s"_root_.mainecoon.Tuple${arity}K")

      val methods = templ.stats.map(_.map {
        case q"def $methodName[..$mTParams](..$params): $f[$resultType]" =>
          val returnItems = range.map { n =>
            q"${Term.Name("af" + n)}.$methodName(..${arguments(params)})"
          }
          q"""def $methodName[..$mTParams](..$params): $productTypeName[..$effectTypeParamsNames]#λ[$resultType] =
           (..$returnItems)"""
        //curried version
        case q"def $methodName[..$mTParams](..$params)(..$params2): $f[$resultType]" =>
          val returnItems = range.map { n =>
            q"${Term.Name("af" + n)}.$methodName(..${arguments(params)})(..${arguments(params2)})"
          }
          q"""def $methodName[..$mTParams](..$params)(..$params2): $productTypeName[..$effectTypeParamsNames]#λ[$resultType] =
           (..$returnItems)"""
        case st => abort(s"autoProductK does not support algebra with such statement: $st")
      }).getOrElse(Nil)

      // tparam"F1[_], F2[_], F3[_]"
      val effectTypeParams: Seq[Type.Param] = range.map(n => typeParam(s"F$n", 1))

      // param"af1: A[F1]"
      def inboundInterpreter(idx: String): Term.Param =
        Term.Param(Nil, Term.Name("af" + idx), Some(Type.Name(s"${name.value}[F"+idx + "]")), None)

      //af1: A[F1], af2: A[F2], af3: A[F3]
      val inboundInterpreters: Seq[Term.Param] = range.map(inboundInterpreter)

      q"""
        def ${Term.Name("product" + arity + "K")}[..$effectTypeParams](..$inboundInterpreters): $name[$productTypeName[..$effectTypeParamsNames]#λ] =
          new ${Ctor.Ref.Name(name.value)}[$productTypeName[..$effectTypeParamsNames]#λ] {
            ..$methods
          }
      """

    }

    val instanceDef = (3 to 9).map(productMethod)

    cls.copy(companion = cls.companion.addStats(instanceDef))
  }
}