/*
 * Copyright 2020 Eike K. & Contributors
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

package docspell.query

import cats.data.{NonEmptyList => Nel}

import docspell.query.ItemQuery.Attr.{DateAttr, StringAttr}

/** A query evaluates to `true` or `false` given enough details about an item.
  *
  * It may consist of (field,op,value) tuples that specify some checks against a specific
  * field of an item using some operator or a combination thereof.
  */
final case class ItemQuery(expr: ItemQuery.Expr, raw: Option[String]) {
  def findFulltext: FulltextExtract.Result =
    FulltextExtract.findFulltext(expr)
}

object ItemQuery {

  sealed trait Operator
  object Operator {
    case object Eq extends Operator
    case object Neq extends Operator
    case object Like extends Operator
    case object Gt extends Operator
    case object Lt extends Operator
    case object Gte extends Operator
    case object Lte extends Operator
  }

  sealed trait TagOperator
  object TagOperator {
    case object AllMatch extends TagOperator
    case object AnyMatch extends TagOperator
  }

  sealed trait Attr
  object Attr {
    sealed trait StringAttr extends Attr
    sealed trait DateAttr extends Attr
    // sealed trait IntAttr extends Attr

    case object ItemName extends StringAttr
    case object ItemSource extends StringAttr
    case object ItemNotes extends StringAttr
    case object ItemId extends StringAttr
    case object Date extends DateAttr
    case object DueDate extends DateAttr
    case object CreatedDate extends DateAttr

    object Correspondent {
      case object OrgId extends StringAttr
      case object OrgName extends StringAttr
      case object PersonId extends StringAttr
      case object PersonName extends StringAttr
    }

    object Concerning {
      case object PersonId extends StringAttr
      case object PersonName extends StringAttr
      case object EquipId extends StringAttr
      case object EquipName extends StringAttr
    }

    object Folder {
      case object FolderId extends StringAttr
      case object FolderName extends StringAttr
    }
  }

  sealed trait Property
  object Property {
    final case class StringProperty(attr: StringAttr, value: String) extends Property
    final case class DateProperty(attr: DateAttr, value: Date) extends Property
    // final case class IntProperty(attr: IntAttr, value: Int) extends Property

    def apply(sa: StringAttr, value: String): Property =
      StringProperty(sa, value)

    def apply(da: DateAttr, value: Date): Property =
      DateProperty(da, value)

    // def apply(na: IntAttr, value: Int): Property =
    //  IntProperty(na, value)
  }

  sealed trait Expr {
    def negate: Expr =
      Expr.NotExpr(this)
  }

  object Expr {
    final case class AndExpr(expr: Nel[Expr]) extends Expr
    final case class OrExpr(expr: Nel[Expr]) extends Expr
    final case class NotExpr(expr: Expr) extends Expr {
      override def negate: Expr =
        expr
    }

    final case class SimpleExpr(op: Operator, prop: Property) extends Expr
    final case class Exists(field: Attr) extends Expr
    final case class InExpr(attr: StringAttr, values: Nel[String]) extends Expr
    final case class InDateExpr(attr: DateAttr, values: Nel[Date]) extends Expr
    final case class InboxExpr(inbox: Boolean) extends Expr
    final case class DirectionExpr(incoming: Boolean) extends Expr

    final case class TagIdsMatch(op: TagOperator, tags: Nel[String]) extends Expr
    final case class TagsMatch(op: TagOperator, tags: Nel[String]) extends Expr
    final case class TagCategoryMatch(op: TagOperator, cats: Nel[String]) extends Expr

    final case class CustomFieldMatch(name: String, op: Operator, value: String)
        extends Expr
    final case class CustomFieldIdMatch(id: String, op: Operator, value: String)
        extends Expr

    final case class Fulltext(query: String) extends Expr
    final case class ChecksumMatch(checksum: String) extends Expr
    final case class AttachId(id: String) extends Expr

    /** A "private" expression is only visible in code, but cannot be parsed. */
    sealed trait PrivateExpr extends Expr
    final case object ValidItemStates extends PrivateExpr
    final case object Trashed extends PrivateExpr
    final case object ValidItemsOrTrashed extends PrivateExpr

    // things that can be expressed with terms above
    sealed trait MacroExpr extends Expr {
      def body: Expr
    }
    final case class NamesMacro(searchTerm: String) extends MacroExpr {
      val body =
        Expr.or(
          like(Attr.ItemName, searchTerm),
          like(Attr.Correspondent.OrgName, searchTerm),
          like(Attr.Correspondent.PersonName, searchTerm),
          like(Attr.Concerning.PersonName, searchTerm),
          like(Attr.Concerning.EquipName, searchTerm)
        )
    }

    final case class CorrMacro(term: String) extends MacroExpr {
      val body =
        Expr.or(
          like(Attr.Correspondent.OrgName, term),
          like(Attr.Correspondent.PersonName, term)
        )
    }

    final case class ConcMacro(term: String) extends MacroExpr {
      val body =
        Expr.or(
          like(Attr.Concerning.PersonName, term),
          like(Attr.Concerning.EquipName, term)
        )
    }

    final case class DateRangeMacro(attr: DateAttr, left: Date, right: Date)
        extends MacroExpr {
      val body =
        and(date(Operator.Gte, attr, left), date(Operator.Lt, attr, right))
    }

    final case class YearMacro(attr: DateAttr, year: Int) extends MacroExpr {
      val body =
        DateRangeMacro(attr, date(year), date(year + 1))

      private def date(y: Int): Date =
        Date(y, 1, 1).fold(throw _, identity)
    }

    def or(expr0: Expr, exprs: Expr*): OrExpr =
      OrExpr(Nel.of(expr0, exprs: _*))

    def and(expr0: Expr, exprs: Expr*): AndExpr =
      AndExpr(Nel.of(expr0, exprs: _*))

    def string(op: Operator, attr: StringAttr, value: String): SimpleExpr =
      SimpleExpr(op, Property(attr, value))

    def like(attr: StringAttr, value: String): SimpleExpr =
      string(Operator.Like, attr, value)

    def date(op: Operator, attr: DateAttr, value: Date): SimpleExpr =
      SimpleExpr(op, Property(attr, value))

    def itemIdEq(itemId1: String, moreIds: String*): Expr =
      if (moreIds.isEmpty) string(Operator.Eq, Attr.ItemId, itemId1)
      else InExpr(Attr.ItemId, Nel(itemId1, moreIds.toList))
  }

}
