/*
 * Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP 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 zio.http.codec

import java.util.UUID

import zio.stacktracer.TracingImplicits.disableAutoTrace

/**
 * A [[zio.http.codec.TextCodec]] defines a codec for a text fragment. The text
 * fragment can be decoded into a value, or the value can be encoded into a text
 * fragment.
 *
 * Unlike parsers, text codecs operate on entire fragments. They do not consume
 * input and leave remainders. Also unlike parsers, text codecs do not fail with
 * error messages, but rather, simply return None if they do not succeed in
 * decoding from a given text fragment. Finally, unlike ordinary parsers, text
 * codecs are fully invertible, and can therefore be used in client generation.
 */
sealed trait TextCodec[A] extends PartialFunction[String, A] { self =>
  def apply(value: String): A

  final def decode(value: String): Option[A] =
    if (isDefinedAt(value)) Some(apply(value)) else None

  def describe: String

  def encode(value: A): String

  def isDefinedAt(value: String): Boolean

  private[http] final def erase: TextCodec[Any] = self.asInstanceOf[TextCodec[Any]]
}

object TextCodec {
  implicit val boolean: TextCodec[Boolean] = BooleanCodec

  def constant(string: String): TextCodec[Unit] = Constant(string)

  implicit val int: TextCodec[Int] = IntCodec

  implicit val long: TextCodec[Long] = LongCodec

  implicit val string: TextCodec[String] = StringCodec

  implicit val uuid: TextCodec[UUID] = UUIDCodec

  final case class Constant(string: String) extends TextCodec[Unit] {

    def apply(value: String): Unit = if (value == string) () else throw new MatchError(value)

    def describe: String = "the constant string \"" + string + "\""

    def encode(value: Unit): String = string

    def isDefinedAt(value: String): Boolean = value == string

    override def toString: String = s"TextCodec.constant(${string})"
  }

  case object StringCodec extends TextCodec[String] {
    def apply(value: String): String = value

    def describe: String = "a string"

    def encode(value: String): String = value

    def isDefinedAt(value: String): Boolean = true

    override def toString: String = "TextCodec.string"
  }

  case object IntCodec extends TextCodec[Int] {
    def apply(value: String): Int = Integer.parseInt(value)

    def describe: String = "an integer"

    def encode(value: Int): String = value.toString

    def isDefinedAt(value: String): Boolean = {
      var i       = 0
      var defined = true
      if (value.length > 1 && value.charAt(0) == '-') i += 1
      while (i < value.length) {
        if (!value.charAt(i).isDigit) {
          defined = false
          i = value.length
        }
        i += 1
      }
      defined && i >= 1
    }

    override def toString: String = "TextCodec.int"
  }

  case object LongCodec extends TextCodec[Long] {
    def apply(value: String): Long = java.lang.Long.parseLong(value)

    def describe: String = "a long integer"

    def encode(value: Long): String = value.toString

    def isDefinedAt(value: String): Boolean = {
      var i       = 0
      var defined = true
      if (value.length > 1 && value.charAt(0) == '-') i += 1
      while (i < value.length) {
        if (!value.charAt(i).isDigit) {
          defined = false
          i = value.length
        }
        i += 1
      }
      defined && i >= 1
    }

    override def toString: String = "TextCodec.long"
  }

  case object BooleanCodec extends TextCodec[Boolean] {
    def apply(value: String): Boolean =
      value.toLowerCase match {
        case "true" | "on" | "yes" | "1"  => true
        case "false" | "off" | "no" | "0" => false
        case _                            => throw new MatchError(value)
      }

    def describe: String = "a boolean value"

    def encode(value: Boolean): String = value.toString

    def isDefinedAt(value: String): Boolean = {
      val valueLower = value.toLowerCase
      valueLower == "1" || valueLower == "0" || valueLower == "true" || valueLower == "false" ||
      valueLower == "no" || valueLower == "off" || valueLower == "yes" || valueLower == "on"
    }

    override def toString: String = "TextCodec.boolean"
  }

  case object UUIDCodec extends TextCodec[UUID] {
    def apply(input: String): UUID = UUID.fromString(input)

    def describe: String = "a UUID"

    def encode(value: UUID): String = value.toString

    def isDefinedAt(value: String): Boolean = {
      var i       = 0
      var defined = true
      var group   = 0
      var count   = 0
      while (i < value.length) {
        val char = value.charAt(i)
        if ((char >= 48 && char <= 57) || (char >= 65 && char <= 70) || (char >= 97 && char <= 102))
          count += 1
        else if (char == 45) {
          if (
            group > 4 || (group == 0 && count != 8) || ((group == 1 || group == 2 || group == 3) && count != 4) || (group == 4 && count != 12)
          ) {
            defined = false
            i = value.length
          }
          count = 0
          group += 1
        } else {
          defined = false
          i = value.length
        }
        i += 1
      }
      defined && i == 36
    }

    override def toString: String = "TextCodec.uuid"
  }

}
