/*
 * Copyright 2018-2020 Scala Steward contributors
 *
 * 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 org.scalasteward.core

import cats._
import cats.effect.Bracket
import cats.implicits._
import fs2.Pipe
import scala.collection.mutable.ListBuffer

package object util {
  final type Nel[+A] = cats.data.NonEmptyList[A]
  final val Nel = cats.data.NonEmptyList

  type ApplicativeThrowable[F[_]] = ApplicativeError[F, Throwable]

  type MonadThrowable[F[_]] = MonadError[F, Throwable]

  type BracketThrowable[F[_]] = Bracket[F, Throwable]

  /** Appends `elem` to `buffer` such that its size does not exceed `maxSize`. */
  def appendBounded[A](buffer: ListBuffer[A], elem: A, maxSize: Int): Unit = {
    if (buffer.size >= maxSize) buffer.remove(0, maxSize / 2)
    buffer.append(elem)
  }

  /** Binds the elements of `gfb` until the first `F[Boolean]` that
    * evaluates to `true`.
    *
    * @example {{{
    * scala> import cats.data.State
    *
    * scala> bindUntilTrue(Nel.of(
    *      |   State((l: List[Int]) => (l :+ 1, false)),
    *      |   State((l: List[Int]) => (l :+ 2, true )),
    *      |   State((l: List[Int]) => (l :+ 3, false)),
    *      |   State((l: List[Int]) => (l :+ 4, true ))
    *      | )).runS(List(0)).value
    * res1: List[Int] = List(0, 1, 2)
    * }}}
    */
  def bindUntilTrue[G[_]: Foldable, F[_]: Monad](gfb: G[F[Boolean]]): F[Boolean] =
    gfb.existsM(identity)

  /** Returns true if there is an element that is both in `fa` and `ga`. */
  def intersects[F[_]: UnorderedFoldable, G[_]: UnorderedFoldable, A: Eq](
      fa: F[A],
      ga: G[A]
  ): Boolean =
    fa.exists(a => ga.exists(b => a === b))

  /** Adds a weight to each element and cuts the stream when the total
    * weight is greater or equal to `limit`.
    *
    * @example {{{
    * scala> fs2.Stream.emits("Hello, world!").through(takeUntil(3) {
    *      |   case 'a' | 'e' | 'i' | 'o' | 'u' => 1
    *      |   case _                           => 0
    *      | }).toList.mkString
    * res1: String = Hello, wo
    * }}}
    */
  def takeUntil[F[_], A, N](limit: N)(weight: A => N)(implicit N: Numeric[N]): Pipe[F, A, A] = {
    import N._
    _.map(a => (a, weight(a)))
      .scan1[(A, N)] { case ((_, total), (a, i)) => (a, total + i) }
      .takeThrough { case (_, total) => total < limit }
      .map { case (a, _) => a }
  }
}