/*
 * Copyright (C) 2020  Prasoon Joshi
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package repository

import org.apache.tinkerpop.gremlin.structure.{
  Vertex
}

import scala.collection.immutable.Map
import models._
import org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality

object NiosxGraph {
  //TODO: Use JanusGraph management API to create a graph schema
  //and clean this mess up!

  //TODO: Move the KeyNames here inside the Node.
  //All configuration data of the NiosxGraph should come
  //from one configurable place.
  case class Node(label: String, edges: List[Node])
  val IMAGE = Node("image", Nil)
  val PARTNER = Node("partner", Nil)
  val SUBJECT = Node("subject", Nil)
  val ANNO_CREATOR = Node("annoCreator", Nil)
  val ANNO_GENERATOR = Node("annoGenerator", Nil)
  val ANNO_BODY = Node("annoBody", Nil)
  val ANNO_LOC_RES = Node("annoLocResBody", Nil)
  val ANNO_TEXTBODY = Node("annoTextualBody", Nil)
  val ANNO_SPECIFIC_RES = Node("annoSpecificResource", Nil)
  val ANNOTATION = Node("anno", 
    List(ANNO_CREATOR, ANNO_GENERATOR, ANNO_LOC_RES, 
      ANNO_TEXTBODY, ANNO_SPECIFIC_RES, ANNO_BODY)
    )
  val DESCRIPTION = Node("description", ANNOTATION.edges)

  val ME = Node(
    "me",
    List(IMAGE, PARTNER, SUBJECT, ANNOTATION, DESCRIPTION)
  )
  val USER = Node( "niosxUser", List.empty)

  def getEdgeLabel(n1: Node, n2: Node): String =
    s"${n1.label}-${n2.label}"


  trait KeyName
  object CommonKeys extends KeyName {
    val GRAPHID: String = "graphId"
    val LABEL: String = "label"
    val CONCEPT: String = GenericConcept.name
    val IRI: String = "iri"
    val TYPE: String = "entityType"
    //TODO: Move DESC to ME
    val DESC: String = "niosxDescription"
    val LOCATION: String = "niosxLocation"
    val ACCESS_RESTRICT: String = "niosxAccessRestrict"
    val USE_RESTRICT: String = "niosxUseRestrict"
    val LANGUAGE: String = "niosxLanguage"
    val UNIT_TYPE: String = "niosxUnitType"
    val FORMAT: String = "niosxFormat"

    val allKeys: List[String] = GRAPHID :: LABEL :: DESC :: LOCATION ::
    ACCESS_RESTRICT :: USE_RESTRICT :: LANGUAGE :: UNIT_TYPE :: FORMAT :: Nil
  }
  object UserKeys extends KeyName {
    //---Keys for NiosxUser---start
    val USERNAME   = "userUsername"
    val PASSHASH   = "userPasswordHash"
    val ROLE       = "userRole"
    val PERMISSION = "userPermission"
  }
//    NiosxUserLabel = "user_agent"
//  val NiosxUsernamePropKey = "user_username"
//  val NiosxUserPassHashPropKey = "user_password_hash"
//  val NiosxUserRolePropKey = "user_role"
//  val NiosxUserPermissionsKey = "user_permission"
  //---Keys for NiosxUser---end
  object ImageKeys extends KeyName {
    val SRC: String = "imageSrc"
    val ALT: String = "imageAlt"
    val SIZE: String = "imageSize"
  }
  object SubjectKeys extends KeyName {
    val PREF_LABEL = "subjectPrefLabel"
    val INSCHEME = "subjectInScheme"
    val SUBJECTID = "subjectId"
  }
  //---Keys for Subject ---end
    object MEKeys extends KeyName {
    val AGENCY_CODE: String = "meAgencyCode"
    val RECORD_ID = "meRecordId"
    val UNIT_ID = "meUnitId"
    val TITLE = "meTitle"
    val CREATOR = "meCreator"
    val DATE_OF_CREATION = "meDateOfCreation"
    val EXTENT = "meExtent"
    val LEVEL = "meLevel"
  }
  val MeToImageRelationship = 
    Relationship(edgeLabel = getEdgeLabel(ME, IMAGE))
  val MeToPartnerRelationship = 
    Relationship(edgeLabel = getEdgeLabel(ME, IMAGE))
  val MeToSubjectRelationship = 
    Relationship(edgeLabel = getEdgeLabel(ME, SUBJECT))
  val MeToAnnoRelationship = 
    Relationship(edgeLabel = getEdgeLabel(ME, ANNOTATION))
  
  object AnnoKeys extends KeyName {
      
    val CONTEXT = "annoContext"
    val IDENTITY = "identity"
    val MOTIVATION = "motivation"
    val CREATED = "created"
    val GENERATED = "generated"
  
  }
  object AnnoCreatorKeys extends KeyName {
    val AnnoCreatorNameKey = "creatorName"
    val AnnoCreatorNickKey = "creatorNick"
  }
  object AnnoGeneratorKeys extends KeyName {
    val GenNameKey = "generatorName"
    val GenHomepageKey = "generatorHomepage"
  }
  val AnnoToAnnoCreatorRel = 
    Relationship(edgeLabel = getEdgeLabel(ANNOTATION,ANNO_CREATOR))
  val AnnoToAnnoGeneratorRel = 
    Relationship(edgeLabel = getEdgeLabel(ANNOTATION,ANNO_GENERATOR))
  

  //TODO: This is a code smell! No edge to an abstract type.
  val AnnoToBodyRel = 
    Relationship(edgeLabel = getEdgeLabel(ANNOTATION, ANNO_BODY))
  val AnnoToTextBodyRelationship = 
    Relationship(edgeLabel = getEdgeLabel(ANNOTATION, ANNO_TEXTBODY))
  val AnnoToLocResBodyRel = 
    Relationship(edgeLabel = getEdgeLabel(ANNOTATION, ANNO_LOC_RES))
  val AnnoToTargetRelationship = 
    Relationship(edgeLabel = getEdgeLabel(ANNOTATION, ANNO_SPECIFIC_RES))
  
  def locResToEntityRel(conceptName: String) =
    Relationship(edgeLabel = getEdgeLabel(ANNO_LOC_RES, Node(conceptName, Nil)))
  
  //TODO: TYPE needs to work for all different types of bodies.
  trait BodyKeys extends KeyName 
  object BodyKeys {
//    val TYPE = CommonKeys.TYPE//"body_type"
//    val BODY_TYPE = "body_resource_type"
  }
  object TextBodyKeys extends KeyName {
    val PURPOSE = "bodyPurpose"
    val VALUE = "bodyValue"
    val FORMAT = "bodyFormat"
    val LANGUAGE = "bodyLanguage"
    val CREATOR = "bodyCreator"
  }
  object LocResBodyKeys extends KeyName {
    val SOURCE = "bodySource"
  }

  object SpecificResourceKeys extends KeyName {
    val SOURCE = "specResSource"
    val TARGETID = "targetId"
  }

  
  /**
   * @param nullable 
   *   Decides if a default value is always required.
   *   This becomes important in Translation. The default value
   *   for nullable is `false`.
   * @param cardinality
   *   Defaults to `Cardinality.list` to suggest that multiple "versions"
   *   of the property associated with this key might exist. These 
   *   different versions can be queried based on the context in which
   *   the query traversals are created.
   */
  case class Key(name: String,
    cardinality: Cardinality = Cardinality.list,
    nullable: Boolean = false)
  type KeyMap = Map[String, Key]
  object KeyMap {
    val empty = Map.empty[String, Key]
  }

  case class NEKey[T <: NiosxEntity](
    nodeLabel: String = "niosxEntity",
    textSearchKeys: KeyMap,
    keys: KeyMap
  )
  object NEKey {
    def apply[T <: NiosxEntity](implicit n: NEKey[T]): NEKey[T] = n
    def instance[T <: NiosxEntity](
      label: String,
      textSearchKeys: KeyMap,
      keys: KeyMap): NEKey[T] =
      new NEKey[T](
        nodeLabel = label,
        textSearchKeys = textSearchKeys,
        keys = keys
      )

    type KeyDef = (String, Cardinality, Boolean)
    val common: KeyMap = 
      Map(
        CommonKeys.GRAPHID -> Key(CommonKeys.GRAPHID, Cardinality.single),
        CommonKeys.LABEL -> Key(CommonKeys.LABEL, Cardinality.single),
        CommonKeys.DESC -> Key(CommonKeys.DESC, Cardinality.single, true),
        CommonKeys.LOCATION -> Key(CommonKeys.LOCATION, Cardinality.single, true),
        CommonKeys.ACCESS_RESTRICT -> Key(CommonKeys.ACCESS_RESTRICT, 
          Cardinality.single, true),
        CommonKeys.USE_RESTRICT -> Key(CommonKeys.USE_RESTRICT,
          Cardinality.single, true),
        CommonKeys.LANGUAGE -> Key(CommonKeys.LANGUAGE, Cardinality.single, true),
        CommonKeys.UNIT_TYPE -> Key(CommonKeys.UNIT_TYPE, 
          Cardinality.single, true),
        CommonKeys.FORMAT -> Key(CommonKeys.FORMAT, Cardinality.single, true)
      )

    implicit val ImageNEKey: NEKey[Image] = {
      
      val rest: KeyMap = 
        Map(
          //TODO: Cardinality is decided by the feature available on this
          //key. For example, in this case, `image_alt` supports the following:
          //1. Translatable => 
          //      Select 1 of several `image_alt` edges based on lang
          //2. Accessibility.{TextToSpeech}
          ImageKeys.ALT -> Key(ImageKeys.ALT, Cardinality.single),
          ImageKeys.SRC -> Key(ImageKeys.SRC, Cardinality.single),
          ImageKeys.SIZE -> Key(ImageKeys.SIZE, Cardinality.set, true)
        )

      val userDefinedTSKeys: Set[String] = Set(ImageKeys.ALT)
      
      val ts: KeyMap =
        // &~ is "Set difference"
        rest -- (rest.keySet &~ userDefinedTSKeys)

      instance(
        IMAGE.label,
        ts,
        (common ++ rest))
    }

    implicit val NiosxUserKey: NEKey[NiosxUser] = {
      val userDefinedTSKeys: Set[String] = Set(UserKeys.USERNAME)
      val rest = Map(
        UserKeys.USERNAME   -> Key(UserKeys.USERNAME, Cardinality.single),
        UserKeys.PASSHASH   -> Key(UserKeys.PASSHASH, Cardinality.single),
        UserKeys.PERMISSION -> Key(UserKeys.PERMISSION, nullable = true)
      )
      val ts: KeyMap =
        rest -- (rest.keySet &~ userDefinedTSKeys)
      instance(USER.label, ts, (common ++ rest))
    }

    implicit val MeNEKey: NEKey[MilliEntity] = {
      val userDefinedTSKeys: Set[String] = 
        Set(MEKeys.TITLE)
      val rest = Map(
        MEKeys.AGENCY_CODE      -> Key(MEKeys.AGENCY_CODE, Cardinality.single),
        MEKeys.CREATOR          -> Key(MEKeys.CREATOR, Cardinality.single),
        MEKeys.DATE_OF_CREATION -> Key(MEKeys.DATE_OF_CREATION),
        MEKeys.EXTENT           -> Key(MEKeys.EXTENT),
        MEKeys.LEVEL            -> Key(MEKeys.LEVEL),
        MEKeys.RECORD_ID        -> Key(MEKeys.RECORD_ID),
        MEKeys.TITLE            -> Key(MEKeys.TITLE, Cardinality.single),
        MEKeys.UNIT_ID          -> Key(MEKeys.UNIT_ID)
      )
      val ts: KeyMap = 
        rest -- (rest.keySet &~ userDefinedTSKeys)
          
      instance( ME.label, ts, (common ++ rest))
    }

    implicit val AnnoNEKey: NEKey[Annotation] = {
      val userDefinedTSKeys = Set.empty[String]
      val rest: KeyMap = Map(
        AnnoKeys.CONTEXT    -> Key(AnnoKeys.CONTEXT, Cardinality.single, true),
        AnnoKeys.IDENTITY   -> Key(AnnoKeys.IDENTITY, Cardinality.single, true),
        AnnoKeys.MOTIVATION -> Key(AnnoKeys.MOTIVATION, Cardinality.single, true),
        AnnoKeys.CREATED    -> Key(AnnoKeys.CREATED, Cardinality.single, true),
        AnnoKeys.GENERATED  -> Key(AnnoKeys.GENERATED, Cardinality.single, true)
      )
      val ts = rest -- (rest.keySet &~ userDefinedTSKeys)

      instance(ANNOTATION.label, ts, (common ++ rest))
    }

    implicit val PartnerNEKey: NEKey[Partner] = {
      val userDefinedTSKeys = Set.empty[String]
      val rest = KeyMap.empty
      val ts = rest -- (rest.keySet &~ userDefinedTSKeys)
      instance(PARTNER.label, ts, (common ++ rest))
    }

    implicit val SubNEKey: NEKey[Subject] = {
      val userDefinedTSKeys = Set(SubjectKeys.PREF_LABEL)
      val rest = Map(
        SubjectKeys.PREF_LABEL -> Key(SubjectKeys.PREF_LABEL, Cardinality.single),
        SubjectKeys.INSCHEME -> Key(SubjectKeys.INSCHEME, Cardinality.single),
        SubjectKeys.SUBJECTID -> Key(SubjectKeys.SUBJECTID, Cardinality.single)
      )  
      val ts = rest -- (rest.keySet &~ userDefinedTSKeys)
      instance(SUBJECT.label, ts, (common ++ rest))
    }
    implicit val AnnoCreatorNEKey: NEKey[AnnoCreator] = {
      val userDefinedTSKeys = Set.empty[String]
      val rest = KeyMap.empty
      val ts = rest -- (rest.keySet &~ userDefinedTSKeys)
      instance(ANNO_CREATOR.label, ts, (common ++ rest))
    }
    implicit val AnnoGenNEKey: NEKey[AnnoGenerator] = {
      val userDefinedTSKeys = Set.empty[String]
      val rest = KeyMap.empty
      val ts = rest -- (rest.keySet &~ userDefinedTSKeys)
      instance(ANNO_GENERATOR.label, ts, (common ++ rest))
    }
    implicit val TextualBodyNEKey: NEKey[TextualBody] = {
      val userDefinedTSKeys = Set(TextBodyKeys.VALUE)
      val rest = Map(
        TextBodyKeys.PURPOSE  -> Key(TextBodyKeys.PURPOSE, Cardinality.single),
        TextBodyKeys.VALUE    -> Key(TextBodyKeys.VALUE, Cardinality.single),
        TextBodyKeys.FORMAT   -> Key(TextBodyKeys.FORMAT, Cardinality.single),
        TextBodyKeys.LANGUAGE -> Key(TextBodyKeys.LANGUAGE, Cardinality.single),
        TextBodyKeys.CREATOR  -> Key(TextBodyKeys.CREATOR, Cardinality.single)
      )
      val ts = rest -- (rest.keySet &~ userDefinedTSKeys)
      instance(ANNO_TEXTBODY.label, ts, (common ++ rest))
    }
    implicit val LocResBodyNEKey: NEKey[LocalResource] = {
      val userDefinedTSKeys = Set.empty[String]
      val rest = Map(
        LocResBodyKeys.SOURCE -> Key(LocResBodyKeys.SOURCE, Cardinality.single)
      )
      val ts = rest -- (rest.keySet &~ userDefinedTSKeys)
      instance(ANNO_LOC_RES.label, ts, (common ++ rest))
    }

    implicit val SpecificResNEKey: NEKey[SpecificResource] = {
      val userDefinedTSKeys = Set.empty[String]
      val rest = Map(
        SpecificResourceKeys.SOURCE   -> Key(SpecificResourceKeys.SOURCE,
          Cardinality.single),
        SpecificResourceKeys.TARGETID -> Key(SpecificResourceKeys.TARGETID,
          Cardinality.single),
      )

      val ts = rest -- (rest.keySet &~ userDefinedTSKeys)
      instance(ANNO_SPECIFIC_RES.label, ts, (common ++ rest))
    }

  }

 
  trait LanguageSensitiveEdge
  trait NiosxEdge
  
  case object Darkness extends NiosxEdge
  
  case class ParentRel(
    pGuid: String,
    rel: Relationship,
    underlying: Option[Vertex] = None) extends NiosxEdge

  case class Relationship(
    edgeLabel: String, concept: Option[NiosxConcept] = None)




  
  //---Keys for Language --- begin
  val LangIdKey = "langId"
  val LangDisplayNameKey = "langDisplayname"
  //---Keys for Language --- end

  //---Keys for Person --- begin
  val PersonDisplayNameKey = "personDisplayname"
  //---Keys for Person --- end

  //---Keys for Place --- begin
  val PlaceDisplayNameKey = "placeDisplayname"
  val PlaceLatKey = "placeLat"
  val PlaceLonKey = "placeLon"
  //---Keys for Place --- end

  val PartnerNameKey = "partnerName"

 }
