/*
 * 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 models

import utils.FileUtils
import java.util.UUID

import play.api.data._

import com.lucidchart.open.xtract.{XmlReader, __}
import com.lucidchart.open.xtract.XmlReader._
import com.lucidchart.open.xtract._

import play.api.libs.functional.syntax._
import cats.syntax.all._
import org.apache.tinkerpop.gremlin.structure.Vertex
import java.{util => ju}
import scala.xml.NodeSeq
import scala.xml.Node
import java.time.LocalDate


//Start --- Infrastructure models
/**
 * These set of classes hold state of execution to be
 * passed around from/to the client. These are not persisted.
 * No `graphId` is needed, they are not a subtype of `NiosxEntity`.
 */
case class Page(edges: Seq[PageEdge], pageInfo: PageInfo)

case class PageEdge(
  node: MilliEntity,
  cursor: String,
  offset: Long,
  isDirectMatch: Boolean,
  annotationMatchCount: Int)

import graphql.SchemaInputObjects._
case class PageInfo(
  pageNumber: Int,
  endCursor: String,
  hasNextPage: Boolean,
  filters: EntityFilter)

//import graphql.SchemaInputObjects.GraphId
//case class EntityFilterAlt(
//  blob: String,
//  lang: Option[Seq[GraphId]] = None,
//  dateRange: Option[DateRange] = None,
//  subjects: Option[Seq[GraphId]] = None,
//  people: Option[Seq[GraphId]] = None,
//  places: Option[Seq[GraphId]] = None,
//  partners: Option[Seq[GraphId]] = None)
case class EntityFilter(
  blob: String,
  lang: Option[Seq[Language]] = None,
  dateRange: Option[DateRange] = None,
  subjects: Option[Seq[Subject]] = None,
  people: Option[Seq[Person]] = None,
  places: Option[Seq[Place]] = None,
  partners: Option[Seq[Partner]] = None) 
//object EntityFilter {
//    def fromFilterInput(in: EntityFilterInput) =
//      new EntityFilter(
//        blob = in.blob,
//        lang = in.lang,
//        dateRange = in.dateRange.map(DateRange.fromDateRangeInput(_)),
//        subjects = in.subjects,
//        people = in.people,
//        places = in.places,
//        partners = in.partners)
//  }
//  mediaTypes: Seq[MediaType.Value] = Nil,
//  resultCount: FilterResultCount)

case class FilterResultCount(
  lang: Long,
  date: Long,
  subjects: Long,
  people: Long,
  places: Long,
  partners: Long,
  mediaTypes: Long)

//End   --- Infrastructure models

trait WebResource {
  def iri: String
}

/** Typeclass that converts a metadata type to a NiosxEntity
 *
 *  Provide implicit instances to convert between different metadata types
 *  and NiosxEntity types.
 */
trait Metadata[M, N] {
  def toEntity(m: M): N
}
object Metadata {
  def apply[M, N](implicit m: Metadata[M, N]): Metadata[M, N] = m

  def instance[M, N](f: M => N): Metadata[M, N] =
    new Metadata[M, N] {
      def toEntity(m: M): N = f(m)
    }

  implicit val Ead3Instance: Metadata[Ead, MilliEntity] = 
    instance({ e: Ead => 
      MilliEntity(
        graphId = FileUtils.getNewGraphId,
        agencyCode = e.control.maintenanceAgency.agencyCode.fold("")(_.text),
        recordId = e.control.recordId.text,
        unitId = "",
        title = "",
        creator = "",
        dateOfCreation = "",
        extent = "",
        level = e.archDesc.attributes.level,
        partner = Partner.mockPartner)
        //None)
    })
}

trait Level
object Level extends Enumeration {
  val Item, Series, SubSeries = Value
}

trait Collection {
  def level: Level
}

object EntityType extends Enumeration {
  type Typ = Value
  val Subject, Place, Language, MilliEntity, Annotation,
  Person, Partner, User, Ni, Client, Software, LocalResource,
  TextualBody, SpecificResource,
  Dataset, Image, Video, Sound, Text = Value
}

import models.EntityType.{Typ => Typ}
import models.{EntityType => ET}
import sangria.macros.derive._

object SangriaMiter {
  implicit val typEnumType = deriveEnumType[models.EntityType.Value]()
}


trait NiosxEntity {
  def graphId: String
  def typ: Typ
  def freeNode: Boolean = true
}

case class Ni(graphId: String = FileUtils.getNewGraphId,
  typ: Typ = ET.Ni,
  title: String,
  image: String,
  IRI: String) extends NiosxEntity

case class Subject(graphId: String = FileUtils.getNewGraphId,
  typ: Typ = ET.Subject,
  prefLabel: String,
  inScheme: String,
  id: String,
  isSelected: Boolean = true) extends NiosxEntity

import repository.UserRole
case class NiosxUser(graphId: String = FileUtils.getNewGraphId,
  typ: Typ = ET.User,
  username: String,
  passHash: String,
  role: UserRole,
  permissions: List[String]) extends NiosxEntity

case class AuthPayload(token: String,
  user: NiosxUser)

object MediaType extends Enumeration {
  val IMAGE, VIDEO, TEXT = Value
}
object ImageSize extends Enumeration {
  val SMALL, MEDIUM, LARGE = Value
}
case class Image(graphId: String = FileUtils.getNewGraphId,
  typ: Typ = ET.Image,
  src: String,
  alt: Option[String] = None,
  size: ImageSize.Value) extends NiosxEntity

case class MilliEntity(graphId: String = FileUtils.getNewGraphId,
  typ: Typ = ET.MilliEntity,
  agencyCode: String,
  recordId: String,
  unitId: String,
  title: String,
  creator: String,
  dateOfCreation: String,
  extent: String,
  level: String,
  partner: Partner,
  subjects: Seq[Subject] = Nil,
  images: Seq[Image] = Nil,
  description: Option[String] = None,
  location: Option[String] = None,
  accessRestrict: Option[String] = None,
  useRestrict: Option[String] = None,
  language: Option[String] = None, //TODO: Change to Language type
  unitType: Option[String] = None,
  format: Option[String] = None,
  annotations: Seq[Annotation] = Nil) extends NiosxEntity

trait FilterLike {
  val isSelected: Boolean = true
}
case class Language(graphId: String = FileUtils.getNewGraphId,
  typ: Typ = ET.Language,
  langId: String,
  displayName: String,
  isSelected: Boolean = true ) extends NiosxEntity

case class DateRange(from: LocalDate, to: LocalDate) 
object DateRange {
  def fromDateRangeInput(in: DateRangeInput) = 
    new DateRange(
      from = LocalDate.parse(in.from),
      to   = LocalDate.parse(in.to)
    )
}

//TODO: Person trait with `LegalPerson`, etc.
case class Person(graphId: String,
  typ: Typ = ET.Person,
  displayName: String,
  isSelected: Boolean = true) extends Agent

case class Place(graphId: String = FileUtils.getNewGraphId,
  typ: Typ = ET.Place,
  lat: Double,
  lon: Double,
  displayName: String,
  isSelected: Boolean = true) extends NiosxEntity

case class Partner(graphId: String = FileUtils.getNewGraphId,
  typ: Typ = ET.Partner,
  displayName: String,
  isSelected: Boolean = true) extends NiosxEntity
object Partner {
  def mockPartner: Partner =
    Partner(displayName = "Mock Partner")
}

//Begin: Annotation types

sealed trait NiosxConcept {
  def name: String
}
object NiosxConcept {
  def apply(name: String): NiosxConcept = name match {
    case "ScopeContent" => ScopeContent
    case "comment" => Comment
    case "subjectHeading" => SubjectHeading
    case c => UserDefinedConcept(c)
    case _ => GenericConcept
  }
}
//This case class can be enriched to include desired properties
//for the edge that this user defined concept defines the annotation
//object to be.
case class UserDefinedConcept(name: String) extends NiosxConcept
case object GenericConcept extends NiosxConcept {
  def name = "annotation"
}
case object ScopeContent extends NiosxConcept {
  def name = "ScopeContent"
}
case object Comment extends NiosxConcept {
  def name = "comment"
}
case object SubjectHeading extends NiosxConcept {
  def name = "subjectHeading"
}

case class Annotation(graphId: String,
  context: String = "http://www.w3.org/ns/anno.jsonld",
  identity: String, 
  typ: Typ = ET.Annotation,
  motivation: String,
  creator: AnnoCreator,
  created: String,
  generator: AnnoGenerator,
  generated: String,
  body: Option[Seq[BodyType]],
  target: Seq[TargetType]) extends NiosxEntity
object Annotation {
  def apply(motivation: String,
    id: String,
    body: Option[Seq[BodyType]], 
    target: Seq[TargetType]): Annotation = {
      val graphId: String = FileUtils.getNewGraphId
      val identity = id + graphId
      new Annotation(graphId = graphId,
        identity = identity,
        motivation = motivation,
        creator = Agent.mockCreator,
        created = ju.Calendar.getInstance().toString(),
        generator = Agent.mockGenerator,
        generated = ju.Calendar.getInstance().toString,
        body = body,
        target = target)
  }
}

trait Agent extends NiosxEntity
object Agent {
  val mockCreator: AnnoCreator =
    AnnoCreator(
      graphId = "creator_test_" + FileUtils.getNewGraphId,
      typ = ET.Software,
      name = "Mr. Mock Agent",
      nickname = "niosX")
  def mockGenerator: AnnoGenerator =
    AnnoGenerator(
      graphId = "generator_test_" + FileUtils.getNewGraphId,
      typ = ET.Software,
      name = "Mr. Mock Generator",
      homepage = "niosX dot awesome")
}
case class AnnoCreator(graphId: String = FileUtils.getNewGraphId,
  typ: Typ,
  name: String,
  nickname: String) extends NiosxEntity with Agent

case class Client(graphId: String = FileUtils.getNewGraphId,
  typ: Typ = ET.Client,
  name: String,
  homepage: String) extends Agent
case class AnnoGenerator(graphId: String = FileUtils.getNewGraphId,
  //override val label: String = "anno_generator",
  typ: Typ,
  name: String,
  homepage: String) extends NiosxEntity with Agent
/*
trait BodyResource
object BodyResource {
  def typ: String
  def purpose: String
  def items: Seq[BodyResource]
  def value: String
  def format: String
  def language: String
  def creator: String
  def source: String //TODO: define Source type
}

trait TargetResource
object TargetResource {
  def typ: String
  def styleClass: String
  def source: String
  //TODO: def state
  //TODO: def selector
}
*/
trait BodyType extends WebResource {
  /**
   * "type" of the Body. From https://www.w3.org/TR/annotation-model/#classes :
   * """The type of the Body or Target resource.
   * The Body or Target MAY have 1 or more types, and if so, 
   * the value SHOULD be drawn from the list of classes below, 
   * but MAY come from other vocabularies."""
   * 
   * TODO: Limit to these with custom vocalubaries:
   * [Dataset, Image, Video, Sound, Text]
   */
  def typ: Typ
  def concept: NiosxConcept
  def entity: Option[NiosxEntity] = None
}
object BodyType {
  def apply(typ: Typ,
    purpose: String,
    value: String,
    format: String,
    language: String,
    creator: String,
    concept: NiosxConcept): BodyType = typ match {
      case ET.TextualBody => {
        val graphId = FileUtils.getNewGraphId
        TextualBody(
          graphId = graphId,
          typ, 
          iri = "iri of text",
          purpose, value, format, language, creator,
          concept = concept)
      }
  }
}

case class ExternalWebResource(graphId: String,
  typ: Typ, // = "AUDIO|VIDEO|DATASET",
  concept: NiosxConcept,
  iri: String) extends NiosxEntity with BodyType

case class LocalResource(
  graphId: String,
  iri: String,
  typ: Typ = ET.LocalResource,
  source: String, //TODO: Change to Source type which has iri of the LocalResource
  concept: NiosxConcept,
  //While the `entity` is an optional field, it is important
  //to understand that an edge from `LocalResource` to a
  //`NiosxEntity` is always created when it is persisted on 
  //the data graph. 
  override val entity: Option[NiosxEntity]
) extends NiosxEntity with BodyType

case class TextualBody(graphId: String,
  typ: Typ = ET.TextualBody,
  iri: String,
  purpose: String,
  value: String,
  format: String,
  language: String,
  creator: String,
  concept: NiosxConcept) extends NiosxEntity with BodyType 

trait TargetType { 
  def typ: Typ
  def graphId: String
}
object TargetType {
  def apply(
    typ: Typ, 
    source: String,
    targetId: String): TargetType = typ match {
      case ET.SpecificResource => 
        SpecificResource(graphId = FileUtils.getNewGraphId,
          typ, source, targetId)
  }
}
case class SpecificResource(graphId: String,
  typ: Typ = ET.SpecificResource,
  source: String,
  targetId: String) extends NiosxEntity with TargetType 

// End: Annotation types


object Audience extends Enumeration {
  val INTERNAL, EXTERNAL, OTHER = Value
}

sealed trait Attributes {
  def vertex: Option[Vertex]
}

case class AmCommonAttrbs(id: Option[String],
  altRender: Option[String],
  audience: Option[Audience.Value],
  lang: Option[String],
  script: Option[String])

object AmCommonAttrbs {
  implicit val reader: XmlReader[AmCommonAttrbs] = {
    for {
      id <- attribute[String]("id").optional
      altRender <- attribute[String]("altrender").optional
      audience <- attribute[String]("audience").optional.map(v => v match {
        case Some("internal") => Some(Audience.INTERNAL)
        case Some("external") => Some(Audience.EXTERNAL)
        case _ => None })
      lang <- attribute[String]("lang").optional
      script <- attribute[String]("script").optional
    } yield AmCommonAttrbs(id, altRender, audience, lang, script)
  }
}

case class CommonAttributes(id: Option[String],
  altRender: Option[String],
  audience: Option[Audience.Value],
  lang: Option[String],
  script: Option[String])

object CommonAttributes {
  implicit val reader: XmlReader[CommonAttributes] = {
    for {
      id <- attribute[String]("id").optional
      altRender <- attribute[String]("altrender").optional
      audience <- attribute[String]("audience").optional.map(v => v match {
        case Some("internal") => Some(Audience.INTERNAL)
        case Some("external") => Some(Audience.EXTERNAL)
        case None => None
        case _ => None })
      lang <- attribute[String]("lang").optional
      script <- attribute[String]("script").optional
    } yield CommonAttributes(id, altRender, audience, lang, script)
  }
}

case class EadAttributes(
  id: Option[String],
  altRender: Option[String],
  audience: Option[Audience.Value],
  lang: Option[String],
  script: Option[String],
  relatedEncoding: Option[String],
  base: Option[String],
  vertex: Option[Vertex]) extends Attributes

object EadAttributes {
  implicit val reader: XmlReader[EadAttributes] = {
    for {
      id <- attribute[String]("id").optional
      altRender <- attribute[String]("altrender").optional
      audience <- attribute[String]("audience").optional.map(v => v match {
        case Some("internal") => Some(Audience.INTERNAL)
        case Some("external") => Some(Audience.EXTERNAL)
        case None => None })
      lang <- attribute[String]("lang").optional
      script <- attribute[String]("script").optional
      relatedEncoding <- attribute[String]("relatedencoding").optional
      base <- attribute[String]("base").optional
    } yield 
    EadAttributes( 
      id, 
      altRender, 
      audience, 
      lang, 
      script, 
      relatedEncoding, 
      base,
      None)
  }
}

case class Ead(attributes: EadAttributes,
  control: Control,
  archDesc: ArchDesc)

object Ead {

  implicit val reader: XmlReader[Ead] = (
    EadAttributes.reader,
    (__ \ "control").read[Control],
    (__ \ "archdesc").read[ArchDesc]
  ).mapN(apply _)
}

object LangEncoding extends Enumeration {
  val iso639_1, iso639_2b, iso639_3, OtherLangEncoding = Value
}

object ScriptEncoding extends Enumeration {
  val iso15924, OtherScriptEncoding = Value
}

object DateEncoding extends Enumeration {
  val iso8601, OtherDateEncoding = Value
}

object CountryEncoding extends Enumeration {
  val iso3166_1, OtherCountryEncoding = Value
}

object RepositoryEncoding extends Enumeration {
  val iso15511, OtherRepositoryEncoding = Value
}

case class ControlAttrbs(graphId: Option[String],
  encodingAnalog: Option[String],
  relatedEncoding: Option[String],
  base: Option[String],
  langEncoding: Option[LangEncoding.Value],
  scriptEncoding: Option[ScriptEncoding.Value],
  dateEncoding: Option[DateEncoding.Value],
  countryEncoding: Option[CountryEncoding.Value],
  repositoryEncoding: Option[RepositoryEncoding.Value],
  vertex: Option[Vertex]) extends Attributes 

object ControlAttrbs {
  implicit val reader: XmlReader[ControlAttrbs] = {
    for {
      encodingAnalog <- attribute[String]("encodinganalog").optional
      relatedEncoding <- attribute[String]("relatedencoding").optional
      base <- attribute[String]("base").optional
      langEncoding <- attribute[String]("langencoding").optional.map( v => v match {
        case Some("iso639-1") => LangEncoding.iso639_1
        case Some("iso639-2b") => LangEncoding.iso639_2b
        case Some("iso639-3") => LangEncoding.iso639_3
        case Some("otherlangencoding") => LangEncoding.OtherLangEncoding })
      scriptEncoding <- attribute[String]("scriptencoding").optional.map( v => v match {
        case Some("iso15924") => ScriptEncoding.iso15924
        case Some("otherscriptencoding") => ScriptEncoding.OtherScriptEncoding })
      dateEncoding <- attribute[String]("dateencoding").optional.map( v => v match {
        case Some("iso8601") => DateEncoding.iso8601
        case Some("otherdateencoding") => DateEncoding.OtherDateEncoding })
      countryEncoding <- attribute[String]("countryencoding").optional.map( v => v match {
        case Some("iso3166-1") => CountryEncoding.iso3166_1
        case Some("othercountryencoding") => CountryEncoding.OtherCountryEncoding })
      repositoryEncoding <- attribute[String]("repositoryencoding")
        .optional
        .map(v => v match {
          case Some("iso15511") => RepositoryEncoding.iso15511
          case Some("otherrepositoryencoding") => 
        RepositoryEncoding.OtherRepositoryEncoding })
    } yield 
    ControlAttrbs(None,
      encodingAnalog,
      relatedEncoding,
      base,
      Some(langEncoding),
      Some(scriptEncoding),
      Some(dateEncoding),
      Some(countryEncoding),
      Some(repositoryEncoding),
      None)
  }
}

case class Control(attributes: ControlAttrbs,
  recordId: RecordId,
  otherRecordId: Option[Seq[OtherRecordId]],
  representation: Option[Seq[Representation]],
//  fileDesc: FileDesc)
//  maintenanceStatus: MaintenanceStatus,
//  publicationStatus: Option[PublicationStatus],
  maintenanceAgency: MaintenanceAgency
//  languageDeclaration: Seq[LanguageDeclaration] = Nil,
//  conventionDeclaration: Seq[ConventionDeclaration] = Nil,
//  rightsDeclaration: Seq[RightsDeclaration] = Nil,
//  localTypeDeclaration: Seq[LocalTypeDeclaration] = Nil,
//  localControl: Seq[LocalControl] = Nil,
//  maintenanceHistory: Seq[MaintenanceHistory] = Nil,
//  sources: Option[Sources]
)

object Control {
  implicit val reader: XmlReader[Control] = (
    ControlAttrbs.reader,
    (__ \ "recordid").read[RecordId],
    (__ \ "otherrecordid").read(XmlReader.seq[OtherRecordId]).optional,
    (__ \ "representation").read(XmlReader.seq[Representation]).optional,
//    (__ \ "filedesc").read[FileDesc]).mapN(apply _)
//    (__ \ "maintenancestatus").read[MaintenanceStatus],
//    (__ \ "publicationstatus").read[PublicationStatus].optional,
    (__ \ "maintenanceagency").read[MaintenanceAgency]).mapN(apply _)
//    (__ \ "languagedeclaration").read(Seq[LanguageDeclaration]),
//    (__ \ "conventiondeclaration").read(Seq[ConventionDeclaration]),
//    (__ \ "rightsdeclaration").read(Seq[RightsDeclaration]),
//    (__ \ "localtypedeclaration").read(Seq[LocalTypeDeclaration]),
//    (__ \ "localcontrol").read(Seq[LocalControl]),
//    (__ \ "maintenancehistory").read[MaintenanceHistory],
//    (__ \ "sources").read[Sources].optional
//  ).mapN(apply _)
}

case class RecordId(graphID: Option[String],
  amCommonAttrbs: AmCommonAttrbs,
  encodingAnalog: Option[String],
  instanceUrl: Option[String], //TODO: Change to ForgiveableUrl
  text: String)

object RecordId {
  implicit val reader: XmlReader[RecordId] = (
    attribute[String]("graphID").optional,
    AmCommonAttrbs.reader,
    attribute[String]("encodinganalog").optional,
    attribute[String]("instanceurl").optional,
    (__).read[String]
  ).mapN(apply _)
}

case class OtherRecordId(graphID: Option[String] = None,
  amCommonAttrbs: AmCommonAttrbs,
  encodingAnalog: Option[String],
  aLocalType: Option[String],
  text: String)

object OtherRecordId {
  implicit val reader: XmlReader[OtherRecordId] = (
    attribute[String]("graphID").optional,
    AmCommonAttrbs.reader,
    attribute[String]("encodinganalog").optional,
    attribute[String]("localtype").optional,
    (__).read[String]
  ).mapN(apply _)
}

case class Representation(graphID: Option[String] = None,
  amCommonAttrbs: AmCommonAttrbs,
  encodingAnalog: Option[String],
  simpleLink: SimpleLink,
  localType: Option[String],
  text: String)

object Representation {
  implicit val reader: XmlReader[Representation] = (
    attribute[String]("graphID").optional,
    AmCommonAttrbs.reader,
    attribute[String]("encodinganalog").optional,
    SimpleLink.reader,
    attribute[String]("localtype").optional,
    (__).read[String]
  ).mapN(apply _)
}

case class MaintenanceAgencyAttrbs(id: Option[String])
object MaintenanceAgencyAttrbs {
  implicit val reader: XmlReader[MaintenanceAgencyAttrbs] = {
    for {
      id <- attribute[String]("id").optional
    } yield MaintenanceAgencyAttrbs(id)
  }
}

case class MaintenanceAgency(graphID: Option[String] = None,
  attributes: MaintenanceAgencyAttrbs,
  agencyCode: Option[AgencyCode])
object MaintenanceAgency {
  implicit val reader: XmlReader[MaintenanceAgency] = (
    attribute[String]("graphID").optional,
    MaintenanceAgencyAttrbs.reader,
    (__ \ "agencycode").read[AgencyCode].optional
  ).mapN(apply _)
}

case class AgencyCodeAttrbs(id: Option[String],
  altRender: Option[String],
  audience: Option[Audience.Value],
  lang: Option[String],
  script: Option[String],
  encodingAnalog: Option[String],
  localType: Option[String],
  vertex: Option[Vertex]) 

object AgencyCodeAttrbs {
  implicit val reader: XmlReader[AgencyCodeAttrbs] = {
    for {
      id <- attribute[String]("id").optional
      altRender <- attribute[String]("altrender").optional
      audience <- attribute[String]("audience").optional.map(v => v match {
        case Some("internal") => Some(Audience.INTERNAL)
        case Some("external") => Some(Audience.EXTERNAL)
        case None => None })
      lang <- attribute[String]("lang").optional
      script <- attribute[String]("script").optional
      encodingAnalog <- attribute[String]("encodinganalog").optional
      localType <- attribute[String]("localtype").optional
    } yield
    AgencyCodeAttrbs(id, 
      altRender, 
      audience, 
      lang, 
      script, 
      encodingAnalog,
      localType,
      None)
  }
}


case class AgencyCode(graphID: Option[String] = None,
  attributes: AgencyCodeAttrbs,
  text: String)
object AgencyCode {
  implicit val reader: XmlReader[AgencyCode] = (
    attribute[String]("graphID").optional,
    AgencyCodeAttrbs.reader,
    (__).read[String]
  ).mapN(apply _)
}

object LinkShow extends Enumeration {
  val NEW, REPLACE, EMBED, OTHER, NONE = Value
}
object Actuate extends Enumeration {
  val ONLOAD, ONREQUEST, OTHER, NONE = Value
}

case class SimpleLink(graphID: Option[String] = None,
  href: Option[String],
  role: Option[String],
  arcrole: Option[String],
  title: Option[String],
  show: Option[LinkShow.Value],
  actuate: Option[Actuate.Value])

object SimpleLink {
  implicit val reader: XmlReader[SimpleLink] = (
    attribute[String]("graphID").optional,
    attribute[String]("href").optional,
    attribute[String]("linkrole").optional,
    attribute[String]("arcrole").optional,
    attribute[String]("linktitle").optional,
    attribute[String]("show").optional.map(v => v match {
      case Some("new") => Some(LinkShow.NEW)
      case Some("replace") => Some(LinkShow.REPLACE)
      case Some("embed") => Some(LinkShow.EMBED)
      case Some("other") => Some(LinkShow.OTHER)
      case Some("none") => Some(LinkShow.NONE)
    }),
    attribute[String]("actuate").optional.map(v => v match {
      case Some("onload") => Some(Actuate.ONLOAD)
      case Some("onrequest") => Some(Actuate.ONREQUEST)
      case Some("other") => Some(Actuate.OTHER)
      case Some("none") => Some(Actuate.NONE)
    })
  ).mapN(apply _)
}

sealed trait ArchDescOption
object ArchDescOption {
  implicit val reader: XmlReader[Seq[ArchDescOption]] = {
    for {
      dsc <- (__ \ "dsc").read[Dsc]
    } yield Seq(dsc)
  }
}

case class ArchDescAttrbs(id: Option[String],
  level: String)
object ArchDescAttrbs {
  implicit val reader: XmlReader[ArchDescAttrbs] = {
    for {
      id <- attribute[String]("id").optional
      level <- attribute[String]("level")
    } yield ArchDescAttrbs(id,
      level)
  }
}

case class ArchDesc(graphID: Option[String] = None,
  attributes: ArchDescAttrbs,
  did: Did,
  archDescOptions: Seq[ArchDescOption] = Nil)
object ArchDesc {
  implicit val reader: XmlReader[ArchDesc] = (
    attribute[String]("graphID").optional,
    ArchDescAttrbs.reader,
    (__ \ "did").read[Did],
    ArchDescOption.reader
  ).mapN(apply _)
}

case class DidAttrbs(id: Option[String],
  audience: Option[String])
object DidAttrbs {
  implicit val reader: XmlReader[DidAttrbs] = {
    for {
      id <- attribute[String]("id").optional
      audience <- attribute[String]("audience").optional
    } yield DidAttrbs(id,
      audience)
  }
}

sealed trait DidOption
object DidOption {
  implicit val reader: XmlReader[Seq[DidOption]] = {
    for {
      unitId <- (__ \ "unitid").read[UnitId]
      unitTitle <- (__ \ "unittitle").read[UnitTitle]
    } yield Seq(unitId, unitTitle)
  }
}

case class Did(graphID: Option[String] = None,
  attributes: DidAttrbs,
  didOptions: Seq[DidOption])
//  unitId: UnitId,
//  unitTitle: UnitTitle,
//  origination: Origination,
//  unitDate: UnitDate,
//  unitDateStructured: UnitDateStructured,
//  physDesc: PhysDesc,
//  physDescSet: PhysDescSet,
//  physDescStructured: PhysDescStructured)
object Did {
  implicit val reader: XmlReader[Did] = (
    attribute[String]("graphID").optional,
    DidAttrbs.reader,
    DidOption.reader
//    (__ \ "unitid").read[UnitId],
//    (__ \ "unittitle").read[UnitTitle],
//    (__ \ "origination").read[Origination],
//    (__ \ "unitdate").read[UnitDate],
//    (__ \ "unitdatestructured").read[UnitDateStructured],
//    (__ \ "physdesc").read[PhysDesc],
//    (__ \ "physdescset").read[PhysDescSet],
//    (__ \ "physdescstructured").read[PhysDescStructured]
  ).mapN(apply _)
}

case class UnitIdAttrbs(countryCode: Option[String],
  repositoryCode: Option[String])
object UnitIdAttrbs {
  implicit val reader: XmlReader[UnitIdAttrbs] = {
    for {
      countryCode <- attribute[String]("countrycode").optional
      repositoryCode <- attribute[String]("repositorycode").optional
    } yield UnitIdAttrbs(countryCode,
      repositoryCode)
  }
}

case class UnitId(graphID: Option[String] = None,
  attributes: UnitIdAttrbs,
  text: String) extends DidOption
object UnitId {
  implicit val reader: XmlReader[UnitId] = (
    attribute[String]("graphID").optional,
    UnitIdAttrbs.reader,
    (__).read[String]
  ).mapN(apply _)
}

case class UnitTitle(graphID: Option[String] = None,
  text: String) extends DidOption
object UnitTitle {
  implicit val reader: XmlReader[UnitTitle] = (
    attribute[String]("graphID").optional,
    (__).read[String]).mapN(apply _)
}

trait OriginationAgent
case class CorpName(graphID: Option[String] = None,
  parts: Seq[Part]) extends OriginationAgent
case class PersName(graphID: Option[String] = None,
  parts: Seq[Part]) extends OriginationAgent
// A graph Key with properties
case class Origination(origin: Seq[OriginationAgent])

case class Part(text: String)

case class Dsc(dummy: Option[String]) extends ArchDescOption//c: Option[Seq[C]]
//  c01: Option[Seq[C01]]) extends ArchDescOption
object Dsc {
  implicit val reader: XmlReader[Dsc] = (
    attribute[String]("id").optional
  ).map(apply _)
}

case class CAttrbs(level: String)
object CAttrbs {
  implicit val reader: XmlReader[CAttrbs] = {
    for {
      level <- attribute[String]("level")
    } yield CAttrbs(level)
  }
}

case class C(attributes: CAttrbs,
  did: Did,
  c: Option[Seq[C]] = None)
case class C01(attributes: CAttrbs,
  did: Did,
  c02: Option[Seq[C02]] = None)
case class C02(attributes: CAttrbs,
  did: Did,
  c03: Option[Seq[C03]] = None)
case class C03(attributes: CAttrbs,
  did: Did,
  c04: Option[Seq[C04]] = None)
case class C04(attributes: CAttrbs,
  did: Did,
  c05: Option[Seq[C05]] = None)
case class C05(attributes: CAttrbs,
  did: Did)


