/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package org.apache.pekko.http.scaladsl.coding

import scala.concurrent.Future

import org.apache.pekko
import pekko.NotUsed
import pekko.annotation.InternalApi
import pekko.http.scaladsl.model._
import pekko.http.impl.util.StreamUtils
import pekko.stream.{ FlowShape, Materializer }
import pekko.stream.scaladsl.{ Flow, Sink, Source }
import pekko.stream.stage.GraphStage
import pekko.util.ByteString
import headers._

import scala.annotation.nowarn

trait Encoder {
  def encoding: HttpEncoding

  def messageFilter: HttpMessage => Boolean

  def encodeMessage(message: HttpMessage): message.Self =
    if (messageFilter(message) && !message.headers.exists(Encoder.isContentEncodingHeader))
      message.transformEntityDataBytes(singleUseEncoderFlow()).withHeaders(
        `Content-Encoding`(encoding) +: message.headers)
    else message.self

  def encodeData[T](t: T)(implicit mapper: DataMapper[T]): T =
    mapper.transformDataBytes(t, Flow.fromGraph(singleUseEncoderFlow()))

  def encoderFlow: Flow[ByteString, ByteString, NotUsed] =
    Flow.fromMaterializer { (_, _) => Flow.fromGraph(singleUseEncoderFlow()) }
      .mapMaterializedValue(_ => NotUsed)

  @InternalApi
  @deprecated(
    "synchronous compression with `encode` is not supported in the future any more, use `encodeAsync` instead",
    since = "Akka HTTP 10.2.0")
  def encode(input: ByteString): ByteString = newCompressor.compressAndFinish(input)

  def encodeAsync(input: ByteString)(implicit mat: Materializer): Future[ByteString] =
    Source.single(input).via(singleUseEncoderFlow()).runWith(Sink.fold(ByteString.empty)(_ ++ _))

  @InternalApi
  @deprecated("newCompressor is internal API", since = "Akka HTTP 10.2.0")
  def newCompressor: Compressor

  @InternalApi
  @deprecated("newEncodeTransformer is internal API", since = "Akka HTTP 10.2.0")
  def newEncodeTransformer(): GraphStage[FlowShape[ByteString, ByteString]] = singleUseEncoderFlow()

  private def singleUseEncoderFlow(): GraphStage[FlowShape[ByteString, ByteString]] = {
    @nowarn("msg=deprecated")
    val compressor = newCompressor

    def encodeChunk(bytes: ByteString): ByteString = compressor.compressAndFlush(bytes)
    def finish(): ByteString = compressor.finish()

    StreamUtils.byteStringTransformer(encodeChunk, () => finish(), () => compressor.cleanup())
  }
}

object Encoder {
  val DefaultFilter: HttpMessage => Boolean = {
    case req: HttpRequest                    => isCompressible(req)
    case res @ HttpResponse(status, _, _, _) => isCompressible(res) && status.allowsEntity
  }
  private[coding] def isCompressible(msg: HttpMessage): Boolean =
    msg.entity.contentType.mediaType.isCompressible

  private[coding] val isContentEncodingHeader: HttpHeader => Boolean = _.isInstanceOf[`Content-Encoding`]
}

/** A stateful object representing ongoing compression. */
@InternalApi
@deprecated("Compressor is internal API and will be moved or removed in the future.", since = "Akka HTTP 10.2.0")
abstract class Compressor {

  /**
   * Compresses the given input and returns compressed data. The implementation
   * can and will choose to buffer output data to improve compression. Use
   * `flush` or `compressAndFlush` to make sure that all input data has been
   * compressed and pending output data has been returned.
   */
  def compress(input: ByteString): ByteString

  /**
   * Flushes any output data and returns the currently remaining compressed data.
   */
  def flush(): ByteString

  /**
   * Closes this compressed stream and return the remaining compressed data. After
   * calling this method, this Compressor cannot be used any further.
   */
  def finish(): ByteString

  /** Combines `compress` + `flush` */
  def compressAndFlush(input: ByteString): ByteString

  /** Combines `compress` + `finish` */
  def compressAndFinish(input: ByteString): ByteString

  /** Release any native resources held by this compressor. Idempotent. */
  private[coding] def cleanup(): Unit = ()
}
