/*
 * 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 scala.util.{Try, Failure, Success}
import scala.jdk.OptionConverters._
import scala.jdk.CollectionConverters._
import utils.FileUtils
import models._
import models.{EntityType => ET}
import repository._
import repository.NiosxGraph._

import org.apache.commons.configuration.BaseConfiguration
import shapeless.Witness
import shapeless.ops.record.Keys
import shapeless.LabelledMacros
import shapeless.LabelledTypeClass
import shapeless.LabelledGeneric
import shapeless.LabelledTypeClassCompanion
import shapeless.MkLabelledGenericLens
import java.{util => ju}
import shapeless.{
  HList,
  HNil,
  ::
}
import shapeless.labelled.FieldType
import shapeless.record._
import org.apache.tinkerpop.gremlin.structure.io.IoCore
import org.apache.tinkerpop.gremlin.structure.{
  Vertex
}
import scala.io.Source
import org.apache.tinkerpop.gremlin.process.traversal.{
  IO,
  Path
}
import org.apache.tinkerpop.gremlin.structure.io.Io
import org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality._
import org.apache.tinkerpop.gremlin.structure.io.graphml.GraphMLIo
import play.api.Configuration
import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.{
  GraphTraversal,
  GraphTraversalSource,
  __
}
import org.janusgraph.core.JanusGraphFactory
import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerVertex
import org.janusgraph.core.VertexList
import org.apache.tinkerpop.gremlin.structure.Direction
import org.janusgraph.core.Cardinality
import org.apache.tinkerpop.gremlin.process.traversal._
import org.janusgraph.core.attribute.Text._
import org.apache.tinkerpop.gremlin.structure.VertexProperty
import org.janusgraph.core.schema.JanusGraphIndex
import org.janusgraph.core.PropertyKey
import org.janusgraph.core.ConfiguredGraphFactory
import org.janusgraph.graphdb.management.JanusGraphManager
import org.janusgraph.core.JanusGraph

//TODO:
//Quick notes on persistence:
// An architectural theme running across the project is synergy between the
// graph data model, community driven functional interactions with the user, 
// and organic nature of these interactions AFK.
// While persisting a sub-graph, persist as much as possible for each valid node,
// throw exceptions for failed nodes and forget. If edge construction fails or if
// cardinality of the edge is violated, store this (inconsistent) state of the 
// graph for the user to review/discard at a later point. This is especially 
// useful in bulk upload scenarios when the type consistency check can be
// performed by the application and JanusGraph can be run with relaxed 
// constraints. The edge failures for the previous ingest cycle would be
// available in a `NiosxUser` field.

case class I18n(conf: Configuration) {
    /**
     * This function ensures that translation resources always return
     * an enriched message even if translations are missing for enabled
     * languages.
     * Missing translations are "translation bugs". Using this approach,
     * the translation management becomes verifiable by the end user who
     * is looking at the UI. This approach also democratises the translation
     * management since every user can submit a "bug report" when they get a 
     * clear message in their selected language along with the equivalent
     * resource in the system default language.
     * A text that is simply missing from the UI is invisible to both the 
     * translation manager and the community tester.
     */
    def fromResource(key: String): String = {
      //TODO: Get lang from user
      //https://iso639-3.sil.org/code/ajp
      val lang: Option[String] = Some("ajp")
      val defaultLang: String = "engb"

      lang match {

        case None => 
          conf.get[String](s"niosx.resource.default.${key}")
        case Some(selectedLang) => {
          val translationErr: String =
             conf
                .get[String](s"niosx.resource.${selectedLang}.TRANSLATION_ERROR")
          conf
            .getOptional[String](s"niosx.resource.${selectedLang}.${key}")
            .getOrElse(translationErr + 
              conf.get[String](s"niosx.resource.default.${key}"))
        }
      }
    }

    val DEFAULT_IMG_ALT ="image.alt"

}

import org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal
class GremlinServer(conf: Configuration) {

  val i18n: I18n = I18n(conf)
  //TODO: Move the db connect code to a component.
  //TODO: Create environment specific db connections.

  val devMode: Boolean = conf.get[Boolean]("dev.mode")

  implicit val graph = devMode match {
    case true  => 
      JanusGraphFactory
        .build()
        .set("storage.backend","inmemory")
        .open()

    case false =>

//      .getGraph(conf.get[String]("graphName"))

//      .ConfiguredGraphFactory.open("asdf")

//      JanusGraphFactory
//        .open("conf/janusgraph-cql-es.properties")
//        traversal()
//          .withRemote("conf/remote-graph.properties")
          
      JanusGraphFactory
        .open("conf/janusgraph-cql-es.properties")
        
      //.asScala
      //.configure(_.withRemote("conf/remote-graph.properties"))
  }
  
  implicit val g = 
    graph
      .traversal

  import org.apache.tinkerpop.gremlin.structure.VertexProperty.{Cardinality => cn}
  val mgmt = graph.openManagement()
  
  def createPropKey(
    name: String, 
    typ: Class[_],
    card: cn): PropertyKey = {
      val key: Option[PropertyKey] = Option(mgmt.getPropertyKey(name))
      key match {
        case None =>
          mgmt
           .makePropertyKey(name)
           .dataType(typ)
           .cardinality(getCast(card))
           .make()
        case Some(key) => key
      }
  }

  def getCast(c: cn) =
    c match {
      case cn.single => Cardinality.SINGLE
      case cn.list   => Cardinality.LIST
      case cn.set    => Cardinality.SET
    }

  def createMixedIndexes[T <: NiosxEntity]
  (implicit ks: NEKey[T]): JanusGraphIndex = {
    val pks = ks.textSearchKeys.values.map(key =>
        createPropKey(key.name, classOf[String], key.cardinality))

    Option(mgmt.getGraphIndex(s"${ks.nodeLabel}MixedIndex")) match {
      case None => {
        val indexBuilder = mgmt
          .buildIndex(s"${ks.nodeLabel}MixedIndex", classOf[Vertex])
        pks.map(k => indexBuilder.addKey(k))
        indexBuilder.buildMixedIndex("search")
      }
      case Some(i) => i
    }
  }

  def createCompositeIndex(key: PropertyKey) = {
    Option(mgmt.getGraphIndex(s"${key.name}Index")) match {
      case None => mgmt
        .buildIndex(s"${key.name}Index", classOf[Vertex])
        .addKey(key)
        .buildCompositeIndex()
      case Some(i) => i
    }
  }
  
  val guid = createPropKey(CommonKeys.GRAPHID, classOf[String], cn.single)
  val prefLabel = createPropKey(SubjectKeys.PREF_LABEL, classOf[String], cn.single)
  val imageSrc = createPropKey(ImageKeys.SRC, classOf[String], cn.single)
  val username = createPropKey(UserKeys.USERNAME, classOf[String], cn.single)
  val entityTyp = createPropKey(CommonKeys.TYPE, classOf[String], cn.single)

  // Graph indexes
  g.getGraph.tx().rollback()
  val guidIndex = createCompositeIndex(guid)
  val prefLabelIndex = createCompositeIndex(prefLabel)
  val imgSrcIndex = createCompositeIndex(imageSrc)
  val usernameIndex = createCompositeIndex(username)
  val entityTypeIndex = createCompositeIndex(entityTyp)

  // Mixed indexes
  if(!devMode) {
    createMixedIndexes[MilliEntity]
    createMixedIndexes[Annotation]
    createMixedIndexes[Subject]
    createMixedIndexes[TextualBody]
    createMixedIndexes[Image]
  }

  mgmt.commit()
  
  val graphMlFile = "/home/anansi/Documents/milli/data.xml"

  val baseUri: String = conf.get[String]("baseUri")


  def graphMl: String = {
    "NotYetImplemented"
//    graph.asJava.io(IoCore.graphml()).readGraph(graphMlFile)
//    graph.asJava.traversal
//      .io(graphMlFile)
//      .read
//      .iterate()
//    
//    Source.fromFile(graphMlFile).mkString
  
  }

//  import idmgmt.{NiosxUser => Identity}
//
//  def findUserByLoginInfo(loginInfo: LoginInfo): Option[NiosxUser] = {
//    g
//      .V()
//      .hasLabel("UserLoginInfo")
//      .has("userKey", loginInfo.providerID)
//    ???
//  }

  import scala.util.chaining._
  def commitTx = {
    g.getGraph.tx().commit()
    //println(s"Transaction open? ${graph.tx().isOpen()}")
  }

  def rollbackTx = {
    g.getGraph.tx().rollback()
    //println(s"[warn]Transaction rolled back!")
  }
  
  def isValidGraphId(graphId: String): Boolean = 
      g
      .V()
      .has(CommonKeys.GRAPHID, graphId)
      .hasNext()
  // Important note for future refactoring:
  // Resist the temptation of changing this into a function that accepts
  // both username and password hash and compares with the values in the 
  // database. This is a security concern and should not be handled in,
  // what essentially is just, a DAO.
  def findUserByUsername(username: String)
  (implicit ks: NEKey[NiosxUser]): Option[NiosxUser] = {
    val vert: Option[Vertex] = 
      g
        .V()
        .has(UserKeys.USERNAME, username)
        .hasLabel(ks.nodeLabel)
        .tryNext()
        .toScala
      
      vert.flatMap(VertToCC[NiosxUser].convert(_))

  }

  case class ResultSet(nodes: GraphTraversal[Vertex, Vertex],
    hasNextPage: Boolean) {
    //TODO: Paging implementation needs to be improved
    //val pageNumber: Int

    def filterBy(filters: EntityFilter): ResultSet = {

      ???
    }

    //TODO: Think of a better cursor strategy.
    //Maybe hook into the pagination feature of cql?
    def paginate(next: Long = 20L, cursorPos: Long = 0L): ResultSet = {
        //range(a,b) is inclusive/exclusive
        val n: GraphTraversal[Vertex, Vertex] = 
          nodes.range(cursorPos, cursorPos+next)
        val hasNext: Boolean = nodes.order().skip(cursorPos+next).hasNext()
        new ResultSet(n, hasNext)
      }

    def get(implicit converter: VertToCC[MilliEntity]): Seq[MilliEntity] = 
      nodes
        .asScala
        .toList
        .flatMap(converter.convert(_))
          
  }

  def findByText(blob: String): ResultSet = {
    new ResultSet(hasNextPage = true,
      nodes = 
        g
          .V().has(MEKeys.TITLE, textContains(blob)).order())

  }

  def traversalToCC[E](tx: GraphTraversal[Vertex, Vertex])
  (implicit converter: VertToCC[E]): Seq[E] =
    tx
      .toList().asScala.toList.flatMap(converter.convert(_))

  def paginatedFind_(blob: String,
    next: Long,
    cursor: Long)(implicit ks: NEKey[MilliEntity]): Seq[MilliEntity] =
      if(blob.isEmpty) {
        traversalToCC[MilliEntity](
          g.V().hasLabel(ks.nodeLabel).order().range(cursor, cursor+next))
      } else {
        val trav = 
          g
          .V()
          .has(MEKeys.TITLE, textContains(blob))
          .order()
          .range(cursor, cursor+next)

        traversalToCC[MilliEntity](trav)
      }
 
  def paginatedFind(blob: String,
    next: Long,
    cursor: Long): Seq[MilliEntity] =
      g
        .V()
        .hasLabel("me")
        //.has(MEKeys.TITLE, textContains(blob))
        .order()
        .range(cursor, cursor+next)
        .toList()
        .asScala.toList.flatMap(v => {
            println(s"Got vertex: ${v.id()}")
            VertToCC[MilliEntity].convert(v)
        })

  def hasMore(blob: String,
    cursor: Long): Boolean =
      
//      if(blob.isEmpty) {
//        g.V().order().skip(cursor).hasNext()
//      } else {
        g
          .V()
          .hasLabel("me")
          .has(MEKeys.TITLE, textContains(blob))
          .order()
          .skip(cursor)
          .hasNext()
//      }
    
  def filterTraversal[T <: NiosxEntity](blob: String)
  (implicit ks: NEKey[T]): GraphTraversal[Vertex, Vertex] = {
      val ts =
        ks
          .textSearchKeys
          .values
          .map(key =>
              __.identity[Vertex]
                .has(key.name, textContains(blob))
                .hasLabel(ks.nodeLabel)
                )

      ts.fold(__.union(__.identity[Vertex]))(_ or _)
    }
 
  /**
   * @return A list of type [E <: NiosxEntity] that are annotated with
   * a LocalResource that encapsulates an object of type [T <: NiosxEntity]
   */
  def filterAnnos[E <: NiosxEntity, T <: NiosxEntity](
    entityToAnnoRel: Relationship = MeToAnnoRelationship,
    blob: String,
    next: Int,
    cursor: Long)
  (implicit entityConverter: VertToCC[E],
    bodyConverter: VertToCC[T],
    ks: NEKey[T]): Seq[E] = {

    val bodies: Seq[Vertex] =
      g
        .V()
        //.hasLabel(NEKey[LocalResource].nodeLabel)
        //The NiosxEntity attached as body wrapped by LocalResource
        //.out() 
//        .hasLabel(ks.nodeLabel)
        .filter(filterTraversal[T](blob))
        .order()
        .range(cursor, cursor+next)
        .toList().asScala.toList

    val result: Seq[E] =
      bodies.flatMap({ b: Vertex => {
 //       val bToCC: Option[T] = bodyConverter.convert(b)
        val ent: Option[E] = 
          g.V(b.id())
            .in()
            .hasLabel(NEKey[LocalResource].nodeLabel) //LocalResource | NiosxEntity
            .in(AnnoToBodyRel.edgeLabel)
            .in(entityToAnnoRel.edgeLabel)
            .tryNext().toScala
            .flatMap(entityConverter.convert(_))
      ent

      }})
    
      result
  }

  def filterEntityGraph(
    blob: String,
    next: Int,
    cursor: Long): Seq[MilliEntity] = {

      val pureMatches: Seq[MilliEntity] =
        g
        .V()
  //      .hasLabel(NEKey[MilliEntity].nodeLabel)
        .filter(filterTraversal[MilliEntity](blob))
        .order()
        .range(cursor, cursor+next)
        .toList().asScala.toList
        .flatMap(VertToCC[MilliEntity].convert(_))

      val imageMatches: Seq[MilliEntity] =
        g
          .V()
    //      .hasLabel(NEKey[Image].nodeLabel)
          .filter(filterTraversal[Image](blob))
          .in(MeToImageRelationship.edgeLabel)
          .order()
          .range(cursor, cursor+next)
          .toList().asScala.toList
          .flatMap(VertToCC[MilliEntity].convert(_))

        val subAnnoMatches: Seq[MilliEntity] =
          g
            .V()
      //      .hasLabel(NEKey[Subject].nodeLabel)
            .filter(filterTraversal[Subject](blob))
            .in()
            .hasLabel(NEKey[LocalResource].nodeLabel)
            .in(AnnoToBodyRel.edgeLabel)
            .in(MeToAnnoRelationship.edgeLabel)
            //.in(MeToSubjectRelationship.edgeLabel)
            .order()
            .range(cursor, cursor+next)
            .toList().asScala.toList
            .flatMap(VertToCC[MilliEntity].convert(_))

        val textBodyAnnoMatches: Seq[MilliEntity] =
          g
            .V()
  //          .hasLabel(NEKey[TextualBody].nodeLabel)
    //        .has(TextBodyKeys.VALUE, textContains(blob))
            .filter(filterTraversal[TextualBody](blob))
            .in(AnnoToBodyRel.edgeLabel)
            .in(MeToAnnoRelationship.edgeLabel)
            .order()
            .range(cursor, cursor+next)
            .toList().asScala.toList
            .flatMap(VertToCC[MilliEntity].convert(_))

        (pureMatches :++ textBodyAnnoMatches :++ imageMatches :++ subAnnoMatches).take(next)


  }
        
        

  trait Find[E] {
    def findByGraphId(id: String): Option[E]
    /**
     * Returns a list of entities of type `E` that match `blob`
     * with any (appropriate) field of `E`. The matches are fuzzy.
     *
     * @param blob string to be fuzzily matched
     * 
     * @return `ResultSet[E]`
     */
    def findByBlob(blob: String): ResultSet
  }
  object Find {
 
    def apply[E](implicit f: Find[E]): Find[E] = f
    def instance[E](i: String => Option[E],
      j: String => ResultSet): Find[E] = 
      new Find[E] {
        def findByGraphId(id: String): Option[E] = i(id)
        def findByBlob(blob: String): ResultSet = j(blob)
      }

    implicit val meFind: Find[MilliEntity] = {
      def i(id: String): Option[MilliEntity] =
        findVertByGraphId(id).flatMap(VertToCC[MilliEntity].convert(_))


      def j(blob: String): ResultSet = 
        new ResultSet(hasNextPage = true,
          nodes = 
            g.V().has(MEKeys.TITLE, textContains(blob)).order())


      instance(i, j)
    }
  }

  trait Search[T] {
    def byId(id: String)(implicit converter: VertToCC[T]): Option[T] = {
      findVertByGraphId(id).flatMap(converter.convert(_))
    }
  }
  object Search {
    def apply[T](implicit s: Search[T]): Search[T] = s

    def instance[T](id: String => Option[T]): Search[T] =
      new Search[T] {
      }

  }

  
//  def search(t: GraphTraversal[Vertex, Vertex], blob: String) =
//    t.has(TitleKey, textContains(blob))

  def findVertByGraphId(graphId: String): Option[Vertex] =
    g
      .V()
      .has(CommonKeys.GRAPHID, graphId)
      .tryNext()
      .toScala

  def connectedVerts(targetId: String,
    edgeLabel: String): Option[List[Vertex]] =
    g
      .V()
      .has(CommonKeys.GRAPHID, targetId)
      .tryNext()
      .toScala
      .map(v => v.vertices(Direction.OUT, edgeLabel).asScala.toList)

  def findById[E <: NiosxEntity](
    id: String)(implicit converter: VertToCC[E]): Option[E] =
    findVertByGraphId(id).flatMap(converter.convert(_))

    //TODO: Try this approach to work better.
    //Every query that can be constructed for the graph would be a segment
    //of the graph. Each segment would be constructed respecting the underlying
    //schema. This means that each query can be converted to the schema (a bit
    //like constructing object instances from a class) by combining the schema
    //using some simple operators (an algebra defined on the schema object).
//    val path: List[Path] = 
//      g
//        .V()
//        .has(CommonKeys.GRAPHID, graphId)
//        .out("annotation")
//        .path()


  def findAnnotations(v: Vertex): Seq[Annotation] = {

    v
      .vertices(Direction.OUT, MeToAnnoRelationship.edgeLabel)
      .asScala
      .flatMap(VertToCC[Annotation].convert(_))
      .toSeq
  }

  def findConnects[E <: NiosxEntity](
    graphId: String,
    edgeLabel: String)(implicit v: VertToCC[E]): Seq[E] = {
      
      val entityVert: Option[Vertex] = findVertByGraphId(graphId)
      val connects: Option[List[Vertex]] = 
        connectedVerts(graphId, edgeLabel)
      connects.getOrElse(Nil).flatMap(v.convert(_))
  }

  def getBody(body: List[Vertex]): Seq[BodyType] = {
    body.map({ v: Vertex =>
      ET.withName(v.value[String](CommonKeys.TYPE)) match {
        case ET.TextualBody => VertToCC[TextualBody].convert(v).get
        case ET.LocalResource => VertToCC[LocalResource].convert(v).get
      }})
  }
 
  def getTarget(ts: List[Vertex]): Seq[TargetType] = 
    ts map { t =>
      ET.withName(t.value(CommonKeys.TYPE))  match {
        case ET.SpecificResource => VertToCC[SpecificResource].convert(t).get
      }
    }


  trait GetValue[E <: NiosxEntity, T] {
    def get(v: Vertex, key: String): T 
  }
  object GetValue {
    def apply[E <: NiosxEntity,T](implicit t: GetValue[E, T]): GetValue[E, T] = t
    def instance[E <: NiosxEntity,T]( f: (Vertex, String) => T)
    (implicit neKey: NEKey[E]): GetValue[E, T] =
      new GetValue[E, T] {

        def get(v: Vertex, key: String): T = f(v, key)
      }

    /**
     * Instance of GetValue when you expect to get a String
     * value. We want only the Translation Management layer
     * to throw an error which is presented to the community
     * tester as a TRANSLATION_ERROR. This TRANSLATION_ERROR
     * can then be transformed by a Text-to-Speech engine or
     * processed by the upstream in a desired fashion.
     */
    implicit val StringValue: GetValue[Image, String] = {
      implicit val neKey = NEKey[Image]

      instance({ (v: Vertex, keyName: String) => {
        Try {
          val k: Key = neKey.keys(keyName)
          println(s"[Translation]Got key: ${k} for translation.\n\n\n")
          v.property(k.name).orElse(
            i18n.fromResource(i18n.DEFAULT_IMG_ALT))
        } match {
          case Failure(msg) => {
            println("Failed due to no key:\n" + msg.getMessage())
            "SERVER_ERROR"
          }
          case Success(res) => res
        }
      }})//(neKey)
    }

  }

  trait VertToCC[E] {
    def convert(vertex: Vertex): Option[E]
  }
  object VertToCC {
    def apply[E](implicit v: VertToCC[E]): VertToCC[E] = v
    def instance[E](f: Vertex => Option[E]): VertToCC[E] =
      new VertToCC[E] {
        def convert(v: Vertex): Option[E] = f(v)
      }

      implicit val VertToNiosxUser: VertToCC[NiosxUser] =
      instance({ v: Vertex =>  {
        Some(new NiosxUser(
          graphId = v.value[String](CommonKeys.GRAPHID),
          username = v.value[String](UserKeys.USERNAME),
          passHash = v.value[String](UserKeys.PASSHASH),
          //TODO: Generate exceptions at this level of granularity.
          //This would ensure that error messages are meaningful and
          //have a high level of specificity.
          //Impl:
          //  v.value[String](Key) could be replaced by:
          //  getOrThrow[V](v: Vertex, key: String): V = Try {
          //    v.value[V](key)
          //  } ...
          //  This would suppress tinkerpop errors. Maybe the exception
          //  framework should have an exception object that has a field
          //  to show underlying errors - simple error messages for the 
          //  end-user, full error stack for the client/developer.
          role = UserRole.getRole(v.value[String](UserKeys.ROLE)),
          permissions = 
            v.values[String](UserKeys.PERMISSION).asScala.toList)
      )}// .toOption
      })

    implicit val VertToMilliEntity: VertToCC[MilliEntity] =
      instance({ v: Vertex => {//Try {

        val propMap = Map.from[String, String](
          v.properties(CommonKeys.allKeys: _*).asScala
          .map({prop: VertexProperty[String] => 
            //TODO: Check for cardinality, type, etc. for each value object
            (prop.key, prop.value)
          })
        )

      propMap.get(CommonKeys.GRAPHID).map( graphId =>
        new MilliEntity(
          graphId        = graphId,
          description    = {
            v.property[String](CommonKeys.DESC)
              .orElse("NIOSX_NONE_CONSTANT") match {
                case "NIOSX_NONE_CONSTANT" => None
                case value => Some(value)
              }
          },
          location       = propMap.get(CommonKeys.LOCATION),
          accessRestrict = propMap.get(CommonKeys.ACCESS_RESTRICT),
          useRestrict    = propMap.get(CommonKeys.USE_RESTRICT),
          language       = propMap.get(CommonKeys.LANGUAGE),
          unitType       = propMap.get(CommonKeys.UNIT_TYPE),           
          format         = propMap.get(CommonKeys.FORMAT),
          agencyCode     = v.value[String](MEKeys.AGENCY_CODE),
          recordId       = v.value[String](MEKeys.RECORD_ID),
          unitId         = v.value[String](MEKeys.UNIT_ID),
          title          = v.value[String](MEKeys.TITLE),
          creator        = v.value[String](MEKeys.CREATOR),
          dateOfCreation = v.value[String](MEKeys.DATE_OF_CREATION),
          extent         = v.value[String](MEKeys.EXTENT),
          level          = v.value[String](MEKeys.LEVEL),
          images         =
            findConnects[Image](graphId, MeToImageRelationship.edgeLabel),
          partner        = 
            findConnects[Partner](graphId,MeToPartnerRelationship.edgeLabel).head,
          subjects       = 
            findConnects[Subject](graphId, MeToSubjectRelationship.edgeLabel),
          annotations    = findAnnotations(v)
        )
      )
      } //.toOption
      })

    implicit val VertToPartner: VertToCC[Partner] =
      instance({ v: Vertex =>  Try {
        Partner(v.value[String](CommonKeys.GRAPHID),
          ET.Partner,
          v.value[String](PartnerNameKey))
      }.toOption
      })
    
    implicit val VertToAnno: VertToCC[Annotation] =
      instance({ v: Vertex => {
        Some(new Annotation(
          graphId = v.value[String](CommonKeys.GRAPHID),
          context = v.value[String](AnnoKeys.CONTEXT),
          typ = ET.Annotation,
          motivation = v.value[String](AnnoKeys.MOTIVATION),
          identity = v.value[String](AnnoKeys.IDENTITY),
          creator = VertToCC[AnnoCreator].convert(
            g.V(v.id).out(AnnoToAnnoCreatorRel.edgeLabel).next()).get,
          created = v.value[String](AnnoKeys.CREATED),
          generator = VertToCC[AnnoGenerator]
            .convert(
              g.V(v.id()).out(AnnoToAnnoGeneratorRel.edgeLabel).next()).get,
          generated = v.value[String](AnnoKeys.GENERATED),
          target = getTarget(
            connectedVerts(
              v.value[String](CommonKeys.GRAPHID),
              AnnoToTargetRelationship.edgeLabel).get),
          body = connectedVerts(
            v.value[String](CommonKeys.GRAPHID),
             AnnoToBodyRel.edgeLabel).map(getBody(_))
           ))
      }//.toOption
 
      })
    implicit val VertToCreator: VertToCC[AnnoCreator] =
      instance({ v: Vertex => Try {
        new AnnoCreator(
          graphId = v.value[String](CommonKeys.GRAPHID),
          typ = ET.withName(v.value[String](CommonKeys.TYPE)),
          name = v.value[String](AnnoCreatorKeys.AnnoCreatorNameKey),
          nickname = v.value[String](AnnoCreatorKeys.AnnoCreatorNickKey)
        )
      } .toOption
      })
    implicit val VertToGenerator: VertToCC[AnnoGenerator] =
      instance({ v: Vertex => Try {
        new AnnoGenerator(
          graphId = v.value[String](CommonKeys.GRAPHID),
          typ = ET.withName(v.value[String](CommonKeys.TYPE)),
          name = v.value[String](AnnoGeneratorKeys.GenNameKey),
          homepage = v.value[String](AnnoGeneratorKeys.GenHomepageKey)
        )
      }.toOption
      })
    implicit val VertToTextBody: VertToCC[TextualBody] =
      instance({ v: Vertex => Try {
        new TextualBody(
          graphId = v.value[String](CommonKeys.GRAPHID),
          typ = ET.withName(v.value[String](CommonKeys.TYPE)),
          iri = v.value[String](CommonKeys.IRI),
          purpose = v.value[String](TextBodyKeys.PURPOSE),
          value = v.value[String](TextBodyKeys.VALUE),
          format = v.value[String](TextBodyKeys.FORMAT),
          language = v.value[String](TextBodyKeys.LANGUAGE),
          creator = v.value[String](TextBodyKeys.CREATOR),
          concept = NiosxConcept(
            v.value[String](CommonKeys.CONCEPT))
          )
      }.toOption
      })
    
    implicit val VertToLocRes: VertToCC[LocalResource] =
      instance({ v: Vertex => //Try {
        Some(LocalResource(
          graphId = v.value[String](CommonKeys.GRAPHID),
          iri = v.value[String](CommonKeys.IRI),
          typ = ET.withName(v.value[String](CommonKeys.TYPE)),
          source = v.value[String](LocResBodyKeys.SOURCE),
          concept = NiosxConcept(v.value[String](CommonKeys.CONCEPT)),
          entity = {
            val entityVert: Vertex = v.vertices(Direction.OUT).next()
            EntityType.withName(entityVert.value[String](CommonKeys.TYPE)) match {
              case ET.Subject => VertToCC[Subject].convert(entityVert)
              case ET.Place => VertToCC[Place].convert(entityVert)
              case ET.MilliEntity => VertToCC[MilliEntity].convert(entityVert)
              case ET.Annotation => VertToCC[Annotation].convert(entityVert)
            }
          }
          ))
      })//.toOption

    def getLREntity(v: Vertex): Option[NiosxEntity] = {
      val s = NEKey[Subject].nodeLabel
      g.V(v.id())
        .outV() //should be .out()
        .tryNext().toScala.flatMap({ e: Vertex => {
          e.label() match {
            case `s` => VertToCC[Subject].convert(e)
            case _ => None
          }
        }})
    }   

    implicit val VertToSpecResource: VertToCC[SpecificResource] =
      instance({ v: Vertex => 
        Some(new SpecificResource(
          graphId = v.value[String](CommonKeys.GRAPHID),
          typ = ET.withName(v.value[String](CommonKeys.TYPE)),
          source = v.value[String](SpecificResourceKeys.SOURCE),
          targetId = v.value[String](SpecificResourceKeys.TARGETID))
        )
      })

    implicit val VerToSubject: VertToCC[Subject] =
      instance({ v: Vertex => Try {
        new Subject(
          graphId   = v.value[String](CommonKeys.GRAPHID),
          prefLabel = v.value[String](SubjectKeys.PREF_LABEL),
          inScheme  = v.value[String](SubjectKeys.INSCHEME),
          id        = v.value[String](SubjectKeys.SUBJECTID))
      }.toOption
      })



    implicit val VertToImage: VertToCC[Image] =
      instance({ v: Vertex => Try {
        new Image(
          graphId = v.value[String](CommonKeys.GRAPHID),
          src     = v.value[String](ImageKeys.SRC),
          alt     =
            //Intentionally forcing alt to not be None even though
            //the data model doesn't enforce this. This is the work
            //towards the NFR of supporting accessibility of the
            //application. This decision to support translatability
            //and enhance accessibility for the visually impared
            //needs to be done at a configuration level. 
            //For that change, NEKey structure needs to be expanded.
            Some(GetValue[Image, String].get(v,ImageKeys.ALT)),
          size    = ImageSize(v.value[Int](ImageKeys.SIZE))
        )
      } match {
        case Success(v) => Some(v)
        case Failure(e) => {
          e.printStackTrace
          None.tap(_ =>
              println(
                s"""[NIOSX_ERROR]\n
                Exception from: ${this.getClass.getCanonicalName} => \n
                ${e.getMessage}\n""")
              )
        }
      }})

    implicit val VertToLang: VertToCC[Language] =
      instance({ v: Vertex => Try {
        new Language(
          graphId     = v.value[String](CommonKeys.GRAPHID),
          langId      = v.value[String](LangIdKey),
          displayName = v.value[String](LangDisplayNameKey)
        )
      }.toOption
      })

    implicit val VertToPerson: VertToCC[Person] =
      instance({ v: Vertex => Try {
        new Person(
          graphId     = v.value[String](CommonKeys.GRAPHID),
          displayName = v.value[String](PersonDisplayNameKey)
        )
      }.toOption
      })

    implicit val VertToPlace: VertToCC[Place] =
      instance({ v: Vertex => Try {
        new Place(
          graphId     = v.value[String](CommonKeys.GRAPHID),
          displayName = v.value[String](PlaceDisplayNameKey),
          lat         = v.value[Double](PlaceLatKey),
          lon         = v.value[Double](PlaceLonKey)
        )
      }.toOption
      })
 
  }

  //To be used in event sourcing
  trait NiosxMessage
  case object NiosxEntitySaved extends NiosxMessage
  case object NiosxEntitySavedToDarkness extends NiosxMessage

  //// Response.
  trait NiosxResponse
  trait SaveResponse

  case class SaveSuccess(
    v: Vertex,
    msg: NiosxMessage = NiosxEntitySaved,
    val c: Unit = commitTx
  ) extends SaveResponse
  
  case class SaveFailure(
    msg: String,
    details: Option[String] = None,
    val r: Unit = rollbackTx
  ) extends SaveResponse

  def init(): List[String] = {

    //Create a superduperuser
    val userInitMsg: String =
      g
      .addV("niosx_user")
      .property(CommonKeys.GRAPHID, "superduperuser")
      .property(UserKeys.USERNAME, "superduperuser")
      .tryNext()
      .toScala match {
        case None => 
          s"*****ERROR***** Could not create superduperuser"
        case Some(_) => 
          s"## Initialized >> superduperuser"
      }

    println(userInitMsg)
    List(userInitMsg)
  }
  
  def addEdge(from: Vertex, to: Vertex, edge: String) =
    from.addEdge(label = edge, inVertex = to)

  trait Save[E <: NiosxEntity] {
    /**
     * @param edge The edge between this entity and its parent node
     * @param entity The entity to be saved 
     */
    def save(entity: E, edge: NiosxEdge)
    (implicit ks: NEKey[E]): SaveResponse
  }
  object Save {

    def apply[E <: NiosxEntity](implicit s: Save[E]): Save[E] = s

    def instance[E <: NiosxEntity](
      s: (E, NiosxEdge, NEKey[E]) => SaveResponse): Save[E] = 

        new Save[E] {


        def save(e: E, edge: NiosxEdge)
        (implicit ks: NEKey[E]): SaveResponse = 

          edge match {
            case Darkness => 
              if(e.freeNode) {
                s(e, Darkness, ks)
              } else {
                SaveFailure(
                  msg = s"Can't add ${ks.nodeLabel} with Guid:${e.graphId} to Darkness"
                  ).tap(f => println(f.msg))
              }

            case p: ParentRel =>
              findVertByGraphId(p.pGuid) match {
                case None =>
                  if(e.freeNode) {
                    s(e, Darkness, ks)
                  } else {
                    SaveFailure(
                    msg = s"No parent for ${ks.nodeLabel} with Guid: ${p.pGuid}",
                    details = 
                      Some(s"Tried to add ${ks.nodeLabel} with ${e.graphId} but it failed"))
                        .tap(f => println(f.details))
                  }

                case parent: Some[Vertex] => 
                  s(e, p.copy(underlying = parent), ks)
              }
            }
              
      }

      //TODO: Place all these helper functions in a separate object
      //object GremlinHelper
      def optionalProperty[T](
        gT: GraphTraversal[Vertex, Vertex],
        key: String,
        p: Option[T]): GraphTraversal[Vertex, Vertex] =
          p.fold(gT)(gT.property(key, _))
  

//        def addPropsToVert(cardinality: Cardinality,
//          key: String,
//          prop: List[String],
//          g: GraphTraversal[Vertex, Vertex]):
//        GraphTraversal[Vertex, Vertex] = 
//          prop.fold(g)(
//            (gt: GraphTraversal[Vertex, Vertex], p: String) =>
//              gt.property(cardinality, key, p)
//            )
    
    implicit val NiosxUserSave: Save[NiosxUser] =
      instance({ (u: NiosxUser, edge: NiosxEdge, ks: NEKey[NiosxUser]) => {

        val v: Option[Vertex] =
          g
            .addV(ks.nodeLabel)
            .property(CommonKeys.GRAPHID, u.graphId)
            .property(CommonKeys.TYPE, u.typ.toString)
            .property(UserKeys.USERNAME, u.username)
            .property(UserKeys.PASSHASH, u.passHash)
            .property(UserKeys.ROLE, UserRole.roleToString(u.role))
            .tryNext()
            .toScala

        v.foreach({vert: Vertex => 
          u.permissions.foreach(vert.property(set, UserKeys.PERMISSION, _))})
       
        commitOrRollback(v, u.typ.toString, edge)
      }
      })

    def connectParent(child: Vertex, edge: ParentRel) = 
      edge
        .underlying
        .map(_.addEdge(edge.rel.edgeLabel, child))
    /**
     * @param v The vertex to be added to the graph
     * @param nodeLabel The label of the node
     * @param edge All nodes have a edge with a parent node (or Darkness)
     */
    def commitOrRollback(
      v: Option[Vertex],
      nodeLabel: String,
      edge: NiosxEdge): SaveResponse =
        v match {
          case None =>
            SaveFailure(s"Could not save ${nodeLabel}")
              .tap(f => println(f.msg))
          case Some(v) => edge match {
            case Darkness => SaveSuccess(v, NiosxEntitySavedToDarkness)
            case p: ParentRel => 
              p.underlying
                .getOrElse(SaveSuccess(v, NiosxEntitySavedToDarkness))
                ({
                  connectParent(v, p)
                  SaveSuccess(v, NiosxEntitySaved)
                })
          }
        }

    implicit val MilliEntitySave: Save[MilliEntity] =
      instance({ (e: MilliEntity, edge: NiosxEdge, ks: NEKey[MilliEntity]) =>
        val vert: Option[Vertex]= 
          g
            .addV(ks.nodeLabel)
            .property(CommonKeys.GRAPHID, e.graphId)
            .property(CommonKeys.TYPE, e.typ.toString)
            .property(MEKeys.AGENCY_CODE, e.agencyCode)
            .property(MEKeys.RECORD_ID, e.recordId)
            .property(MEKeys.UNIT_ID, e.unitId)
            .property(MEKeys.TITLE, e.title)
            .property(MEKeys.CREATOR, e.creator)
            .property(MEKeys.DATE_OF_CREATION, e.dateOfCreation)
            .property(MEKeys.EXTENT, e.extent)
            .property(MEKeys.LEVEL, e.level)
            .tryNext().toScala

        vert.map({ v: Vertex => {
          e.description.map(v.property(CommonKeys.DESC, _))
          e.location.map(v.property(CommonKeys.LOCATION, _))
          e.accessRestrict.map(v.property(CommonKeys.ACCESS_RESTRICT, _))
          e.useRestrict.map(v.property(CommonKeys.USE_RESTRICT, _))
          e.language.map(v.property(CommonKeys.LANGUAGE, _))
          e.unitType.map(v.property(CommonKeys.UNIT_TYPE, _))
          e.format.map(v.property(CommonKeys.FORMAT, _))
        }})

        //Create Partner
        Save[Partner].save(
          e.partner, 
          ParentRel(e.graphId, MeToPartnerRelationship)
          )

        //Create Annotations if available
        e.annotations
          .map(Save[Annotation].save(_, 
            ParentRel(e.graphId, MeToAnnoRelationship)))

        //Create Subjects if available
        e.subjects
          .map(Save[Subject].save(_,
            ParentRel(e.graphId, MeToSubjectRelationship)))

        //Create Image objects if available
        e.images
          .map(Save[Image].save(_,
            ParentRel(e.graphId, MeToImageRelationship)))

        //TODO: mapEdges to Collection

        commitOrRollback(vert, ks.nodeLabel, edge)

      })

    implicit val CreatorSave: Save[AnnoCreator] =
      instance({ (e: AnnoCreator, edge: NiosxEdge, ks: NEKey[AnnoCreator]) =>
        val entity: Option[Vertex] =
          g
            .addV(ks.nodeLabel)
            .property(CommonKeys.GRAPHID, e.graphId)
            .property(CommonKeys.TYPE, e.typ.toString)
            .property(AnnoCreatorKeys.AnnoCreatorNameKey, e.name)
            .property(AnnoCreatorKeys.AnnoCreatorNickKey, e.nickname)
            .tryNext()
            .toScala
                 
        //mapEdges(entity, pGuid, AnnoCreatorEdgeKey)
        commitOrRollback(entity, ks.nodeLabel, edge)
      })
    implicit val GeneratorSave: Save[AnnoGenerator] =
      instance({ (e: AnnoGenerator, edge: NiosxEdge, ks: NEKey[AnnoGenerator]) =>
        val entity: Option[Vertex] =
          g
            .addV(ks.nodeLabel)
            .property(CommonKeys.GRAPHID, e.graphId)
            .property(CommonKeys.TYPE, e.typ.toString)
            .property(AnnoGeneratorKeys.GenNameKey, e.name)
            .property(AnnoGeneratorKeys.GenHomepageKey, e.homepage)
            .tryNext()
            .toScala

        //mapEdges(entity, pGuid, AnnoGenEdgeKey)
        commitOrRollback(entity, ks.nodeLabel, edge)
      })

    def mapEdges(child: Option[Vertex], 
      pGuid: Option[String],
      edgeName: String): Unit =
      child.map({ v: Vertex =>
        pGuid.flatMap(findVertByGraphId(_).map(_.addEdge(edgeName, v)))
      })

    implicit val TextualBodySave: Save[TextualBody] =
      instance({ (e: TextualBody, edge: NiosxEdge, ks: NEKey[TextualBody]) =>
        val entity: Option[Vertex] =
          g
            .addV(ks.nodeLabel)
            .property(CommonKeys.GRAPHID, e.graphId)
            .property(CommonKeys.IRI, e.iri)
            .property(CommonKeys.TYPE, e.typ.toString)
            .property(TextBodyKeys.PURPOSE, e.purpose)
            .property(TextBodyKeys.VALUE, e.value)
            .property(TextBodyKeys.FORMAT, e.format)
            .property(TextBodyKeys.LANGUAGE, e.language)
            .property(TextBodyKeys.CREATOR, e.creator)
            .property(CommonKeys.CONCEPT, e.concept.name)
            .tryNext()
            .toScala

        commitOrRollback(entity, ks.nodeLabel, edge)
      })

    implicit val LocResSave: Save[LocalResource] = 
      instance({ (e: LocalResource, edge: NiosxEdge, ks: NEKey[LocalResource]) =>
        val entity: Option[Vertex] = 
          g
            .addV(ks.nodeLabel)
            .property(CommonKeys.GRAPHID, e.graphId)
            .property(CommonKeys.IRI, e.iri)
            .property(CommonKeys.TYPE, e.typ.toString)
            .property(LocResBodyKeys.SOURCE, e.source)
            .property(CommonKeys.CONCEPT, e.concept.name)
            .tryNext()
            .toScala

        e.entity.map({ et: NiosxEntity => {
          et.typ match {
            case ET.Subject => 
              Save[Subject].save(
                et.asInstanceOf[Subject], 
                ParentRel(pGuid = e.graphId, 
                  NiosxGraph.locResToEntityRel(e.concept.name))
              )
          }
        }})

        commitOrRollback(entity, ks.nodeLabel, edge)
      })

    implicit val SpecificResourceSave: Save[SpecificResource] =
      instance({ (e: SpecificResource, edge: NiosxEdge, ks: NEKey[SpecificResource]) =>
        val entity: Option[Vertex] =
          g
            .addV(ks.nodeLabel)
            .property(CommonKeys.GRAPHID, e.graphId)
            .property(CommonKeys.TYPE, e.typ.toString)
            .property(SpecificResourceKeys.SOURCE, e.source)
            .property(SpecificResourceKeys.TARGETID, e.targetId)
            .tryNext()
            .toScala
        commitOrRollback(entity, ks.nodeLabel, edge)
      })

    implicit val SubjectSave: Save[Subject] =
      instance({ (s: Subject, edge: NiosxEdge, ks: NEKey[Subject]) =>
        def addSub(s: Subject) =
          __
            .addV(ks.nodeLabel)
            .property(CommonKeys.GRAPHID, s.graphId)
            .property(CommonKeys.TYPE, s.typ.toString)
            .property(SubjectKeys.PREF_LABEL, s.prefLabel)
            .property(SubjectKeys.INSCHEME, s.inScheme)
            .property(SubjectKeys.SUBJECTID, s.id)

        val entity: Option[Vertex] =
          g
            .V()
            .hasLabel(ks.nodeLabel)
            .has(SubjectKeys.PREF_LABEL, s.prefLabel)
            .fold()
            .coalesce(__.unfold(), addSub(s))
            .tryNext
            .toScala
        
//        mapEdges(entity, pGuid, ME_SubjectEdgeKey)
        commitOrRollback(entity, s.typ.toString, edge)
      })

    implicit val ImageSave: Save[Image] = 
      instance({ (i: Image, edge: NiosxEdge, ks: NEKey[Image]) => {
        //TODO: Use Try for all IO. If an exception occurs here,
        //the transaction rollback in `commitOrRollback()` would
        //not get triggered.
        def addImage(i: Image) =
          __.addV(ks.nodeLabel)
            .property(CommonKeys.GRAPHID, i.graphId)
            .property(CommonKeys.TYPE, i.typ.toString)
            .property(ImageKeys.SRC, i.src)
            .tap(optionalProperty[String](_, ImageKeys.ALT, i.alt))
            .property(ImageKeys.SIZE, i.size.id)

          val entity: Option[Vertex] =
            g
              .V()
              .hasLabel(ks.nodeLabel)
              .has(ImageKeys.SRC, i.src)
              .fold()
              .coalesce(__.unfold(), addImage(i))
              .tryNext
              .toScala

            commitOrRollback(entity, i.typ.toString, edge)
      }
      })

    implicit val PartnerSave: Save[Partner] =
      instance({ (p: Partner, edge: NiosxEdge, ks: NEKey[Partner]) => {
        def addPartner(p: Partner) =
          __
            .addV(ks.nodeLabel)
            .property(CommonKeys.GRAPHID, p.graphId)
            .property(PartnerNameKey, p.displayName)
        val entity: Option[Vertex] =
          g
            .V()
            .hasLabel(ks.nodeLabel)
            .has(CommonKeys.GRAPHID, p.graphId)
            .fold()
            .coalesce(__.unfold(), addPartner(p))
            .tryNext
            .toScala
        //mapEdges(entity, pGuid, ME_PartnerEdgeKey)
        commitOrRollback(entity, p.typ.toString, edge)
      }})

    implicit val AnnoSave: Save[Annotation] =
      instance({ (e: Annotation, edge: NiosxEdge, ks: NEKey[Annotation]) =>
        val entity: Option[Vertex] =
          g
            .addV(ks.nodeLabel) //TODO: Save multi labels for NiosxConcept
            .property(CommonKeys.GRAPHID, e.graphId)
            .property(CommonKeys.TYPE, e.typ.toString)
            .property(AnnoKeys.CONTEXT, e.context)
            .property(AnnoKeys.IDENTITY, e.identity)
            .property(AnnoKeys.MOTIVATION, e.motivation)
            .property(AnnoKeys.CREATED, e.created)
            .property(AnnoKeys.GENERATED, e.generated)
            .tryNext()
            .toScala
        e.body.map(_.map(saveBody(_, e.graphId)))
        e.target.map(saveTarget(_, e.graphId))
        
        Save[AnnoCreator]
          .save(e.creator, ParentRel(e.graphId, AnnoToAnnoCreatorRel))
        Save[AnnoGenerator]
          .save(e.generator, ParentRel(e.graphId, AnnoToAnnoGeneratorRel))

        commitOrRollback(entity, ks.nodeLabel, edge)
      })

    def saveBody(body: BodyType, 
      pGuid: String): Option[Vertex] = 
        (body match {
          //TODO: Redundant case branches
          case a: TextualBody => 
            Save[TextualBody]
              .save(a, ParentRel(pGuid, AnnoToBodyRel)) 
          case b: LocalResource =>
            Save[LocalResource]
              .save(b, ParentRel(pGuid, AnnoToBodyRel))
        }) match {
          case s: SaveSuccess => Some(s.v)
          case f: SaveFailure => None.tap(_ => println(f.msg))
        }

    def saveTarget(target: TargetType,
      pGuid: String): Option[Vertex] = 
      target match {
        case t: SpecificResource=> 
          Save[SpecificResource]
            .save(t, ParentRel(pGuid, AnnoToTargetRelationship)) match {
              case s: SaveSuccess => Some(s.v)
              case f: SaveFailure => None
            }
      }
  }
}
  /*
  def fetchElement(uri: String): MilliEntity = ???

  def persistElement[E <: MilliEntity,
  A <: Attributes,
  W <: Symbol,
  R <: HList](ele: E)(implicit
    gen: LabelledGeneric.Aux[E, FieldType[W, A] :: R],
    hPersistence: Persistence[Attributes],
    edgeMapper: EdgeMapper[R]): MilliEntity = {
      val repr = gen.to(ele)
      val attr: Attributes = hPersistence.persist(ele.attributes)
      attr.vertex.map(vertex => edgeMapper.mapEdges(vertex, repr.tail))
      fetchElement(baseUri + attr.graphId)
    }


  trait Persistence[E] {
    def persist(entity: E): Attributes
  }
  object Persistence {
    def apply[E](implicit p: Persistence[E]): Persistence[E] = p
    def instance[E](f: E => Attributes): Persistence[E] =
      new Persistence[E] {
        def persist(entity: E): Attributes = f(entity)
      }
    implicit val eadPersistence: Persistence[EadAttributes] =
      instance({ attr: EadAttributes => 
        (graph + attr).toCC[EadAttributes]
      })

  }

  trait EdgeMapper[R] {

    def mapEdges(vertex: Vertex, edges: R): Unit
  }

  object EdgeMapper {

    def apply[R](implicit e: EdgeMapper[R]): EdgeMapper[R] = e

    def instance[R](f: (Vertex, R) => Unit): EdgeMapper[R] =
      new EdgeMapper[R] {
        def mapEdges(v: Vertex, edges: R): Unit =
          f(v, edges)
      }

    implicit val EdgelessNode: EdgeMapper[HNil] =
      instance((vertex, hnil) => ())

    implicit def edgesHListMapper[W <: Symbol,
    H <: MilliEntity,
    A <: Attributes,
    R <: HList](implicit
    witness: Witness.Aux[W],
    edgeMapper: EdgeMapper[R],
    hPersistence: Persistence[H],
    aPersistence: Persistence[Attributes],
    gen: LabelledGeneric.Aux[H, FieldType[W, A] :: R]
    ): EdgeMapper[FieldType[W, H] :: R] = {
      //TODO: Lookup from a dictionary of typed edge names.
      val edgeName: String = witness.value.name
      instance{ (vertex : Vertex, edges) => 
        persistElement(getFieldValue(edges.head)).attributes.vertex.map(edge =>
            vertex --- edgeName --> edge)
        edgeMapper.mapEdges(vertex, edges.tail)
      }
    }

    def getFieldValue[K, V](value: FieldType[K, V]): V = value
  }
}
*/
