/*
 * 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
package scaladsl
package model
package sse

import org.apache.pekko
import pekko.http.javadsl.model
import pekko.util.ByteString
import pekko.util.OptionConverters._
import java.nio.charset.StandardCharsets.UTF_8
import scala.annotation.tailrec

object ServerSentEvent {

  /**
   * A [[ServerSentEvent]] with empty data which can be used as a heartbeat.
   */
  val heartbeat: ServerSentEvent =
    ServerSentEvent("")

  /**
   * Creates a [[ServerSentEvent]].
   *
   * @param data data, may span multiple lines
   * @param type type, must not contain \n or \r
   */
  def apply(data: String, `type`: String): ServerSentEvent =
    new ServerSentEvent(data, Some(`type`))

  /**
   * Creates a [[ServerSentEvent]].
   *
   * @param data data, may span multiple lines
   * @param type type, must not contain \n or \r
   * @param id id, must not contain \n or \r
   */
  def apply(data: String, `type`: String, id: String): ServerSentEvent =
    new ServerSentEvent(data, Some(`type`), Some(id))

  /**
   * Creates a [[ServerSentEvent]].
   *
   * @param data data, may span multiple lines
   * @param retry reconnection delay in milliseconds
   */
  def apply(data: String, retry: Int): ServerSentEvent =
    new ServerSentEvent(data, retry = Some(retry))

  private def noNewLine(s: String) = s.forall(c => c != '\n' && c != '\r')

  // Public domain algorithm: http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2.
  // We want powers of two both because they typically work better with the allocator, and because we want to minimize
  // reallocations/buffer growth.
  private def nextPowerOfTwoBiggerThan(n: Int) = {
    var m = n - 1
    m |= m >> 1
    m |= m >> 2
    m |= m >> 4
    m |= m >> 8
    m |= m >> 16
    m + 1
  }
}

/**
 * Representation of a server-sent event. By default Pekko HTTP uses events with an empty data field as a heartbeat that is
 * ignored on reception. Set `pekko.http.sse.emit-empty-events` can be used to change that default behavior.
 *
 * @param data data, may span multiple lines
 * @param eventType optional type, must not contain \n or \r
 * @param id optional id, must not contain \n or \r
 * @param retry optional reconnection delay in milliseconds
 */
final case class ServerSentEvent(
    data: String,
    eventType: Option[String] = None,
    id: Option[String] = None,
    retry: Option[Int] = None)
    extends model.sse.ServerSentEvent {
  import ServerSentEvent._

  require(eventType.forall(noNewLine), "eventType must not contain \\n or \\r!")
  require(id.forall(noNewLine), "id must not contain \\n or \\r!")
  require(retry.forall(_ > 0), "retry must be a positive number!")

  private[scaladsl] def encode = {
    def s = { // Performance fun fact: change this to val and get an ~30% performance penalty!!!
      // Why 8? "data:" == 5 + \n\n (1 data (at least) and 1 ending) == 2 and then we add 1 extra to allocate
      //        a bigger memory slab than data.length since we're going to add data ("data:" + "\n") per line
      // Why 7? "event:" + \n == 7 chars
      // Why 4? "id:" + \n == 4 chars
      // Why 17? "retry:" + \n + Integer.Max decimal places
      val builder =
        new StringBuilder(
          nextPowerOfTwoBiggerThan(
            8 + data.length + eventType.fold(0)(_.length + 7) + id.fold(0)(_.length + 4) + retry.fold(0)(_ => 17)))
      @tailrec def appendData(s: String, index: Int = 0): Unit = {
        @tailrec def addLine(index: Int): Int =
          if (index >= s.length)
            -1
          else {
            val c = s.charAt(index)
            builder.append(c)
            if (c == '\n') index + 1 else addLine(index + 1)
          }
        builder.append("data:")
        addLine(index) match {
          case -1 => builder.append('\n')
          case i  => appendData(s, i)
        }
      }
      appendData(data)
      if (eventType.isDefined && eventType.get.nonEmpty) builder.append("event:").append(eventType.get).append('\n')
      if (id.isDefined) builder.append("id:").append(id.get).append('\n')
      if (retry.isDefined) builder.append("retry:").append(retry.get).append('\n')
      builder.append('\n').toString
    }
    ByteString(s, UTF_8.name)
  }

  override def getData = data

  override def getEventType = eventType.toJava

  override def getId = id.toJava

  override def getRetry = retry.toJavaPrimitive
}
