/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.livy.server

import javax.servlet.http.{HttpServletRequest, HttpServletResponse}

import scala.concurrent.ExecutionContext
import scala.reflect.ClassTag

import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.databind.{JsonMappingException, ObjectMapper}
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
import org.scalatra._

/**
 * An abstract servlet that provides overridden implementations for "post", "put" and "patch"
 * that can deserialize JSON data directly into user-defined types, without having to go through
 * a json4s intermediate. Results are also automatically serialized into JSON if the content type
 * says so.
 *
 * Serialization and deserialization are done through Jackson directly, so all Jackson features
 * are available.
 */
abstract class JsonServlet extends ScalatraServlet with ApiFormats with FutureSupport {

  override protected implicit def executor: ExecutionContext = ExecutionContext.global

  case class ResponseMessage(msg: String)

  private lazy val _defaultMapper = new ObjectMapper()
    .registerModule(com.fasterxml.jackson.module.scala.DefaultScalaModule)

  /**
   * Override this method if you need a custom Jackson object mapper; the default mapper
   * has the default configuration, plus the Scala module.
   */
  protected def createMapper(): ObjectMapper = _defaultMapper

  protected final val mapper = createMapper()

  before() {
    contentType = formats("json")
  }

  error {
    case e: JsonParseException => BadRequest(e.getMessage)
    case e: UnrecognizedPropertyException => BadRequest(e.getMessage)
    case e: JsonMappingException => BadRequest(e.getMessage)
    case e =>
      SessionServlet.error("internal error", e)
      InternalServerError(e.toString)
  }

  protected def jpatch[T: ClassTag](t: RouteTransformer*)(action: T => Any): Route = {
    patch(t: _*) {
      doAction(request, action)
    }
  }

  protected def jpost[T: ClassTag](t: RouteTransformer*)(action: T => Any): Route = {
    post(t: _*) {
      doAction(request, action)
    }
  }

  protected def jput[T: ClassTag](t: RouteTransformer*)(action: T => Any): Route = {
    put(t: _*) {
      doAction(request, action)
    }
  }

  override protected def renderResponseBody(actionResult: Any): Unit = {
    val result = actionResult match {
      case ActionResult(status, ResponseMessage(msg), headers) if format == "json" =>
        ActionResult(status, toJson(Map("msg" -> msg)), headers)
      case ActionResult(status, body, headers) if format == "json" =>
        ActionResult(status, toJson(body), headers)
      case str: String if format == "json" =>
        // This should be changed when we implement LIVY-54. For now, just create a dummy
        // JSON object when a raw string is being returned.
        toJson(Map("msg" -> str))
      case other if format == "json" =>
        toJson(other)
      case other =>
        other
    }
    super.renderResponseBody(result)
  }

  protected def bodyAs[T: ClassTag](req: HttpServletRequest)
      (implicit klass: ClassTag[T]): T = {
    bodyAs(req, klass.runtimeClass)
  }

  private def bodyAs[T](req: HttpServletRequest, klass: Class[_]): T = {
    mapper.readValue(req.getInputStream(), klass).asInstanceOf[T]
  }

  private def doAction[T: ClassTag](
      req: HttpServletRequest,
      action: T => Any)(implicit klass: ClassTag[T]): Any = {
    action(bodyAs[T](req, klass.runtimeClass))
  }

  private def isJson(res: HttpServletResponse, headers: Map[String, String] = Map()): Boolean = {
    val ctypeHeader = "Content-Type"
    headers.get(ctypeHeader).orElse(Option(res.getHeader(ctypeHeader)))
      .map(_.startsWith("application/json")).getOrElse(false)
  }

  private def toResult(obj: Any, res: HttpServletResponse): Any = obj match {
    case async: AsyncResult =>
      new AsyncResult {
        val is = async.is.map(toResult(_, res))
      }
    case ActionResult(status, body, headers) if isJson(res, headers) =>
      ActionResult(status, toJson(body), headers)
    case body if isJson(res) =>
      Ok(toJson(body))
    case other =>
      other
  }

  private def toJson(obj: Any): Any = {
    if (obj != null && obj != (())) {
      mapper.writeValueAsBytes(obj)
    } else {
      null
    }
  }

}
