//
// Copyright (c) 2011, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   11 Aug 11  Brian Frank  Creation
//   19 Dec 11  Brian Frank  Redesign to make DocPod const
//

**
** DocPod models the documentation of a `sys::Pod`.
**
const class DocPod : DocSpace
{

  ** Load from a zip file using the given DocEnv as the  gerror handler
  static DocPod load(DocEnv? env, File file)
  {
    loadFile(file) |err|
    {
      if (env == null)
        echo("ERROR: $err")
      else
        env.errReport(err)
    }
  }

  ** Load from a zip file with given error handler
  static DocPod loadFile(File file, |DocErr| onErr)
  {
    make(file, onErr)
  }

  ** Private constructor to copy loader fields
  @NoDoc new make(File file, |DocErr| onErr)
  {
    this.file = file
    loader := DocPodLoader(file, this, onErr)
    zip := Zip.open(file)
    try
    {
      // first load meta data
      loader.loadMeta(zip)
      this.name    = loader.name
      this.version = loader.version
      this.summary = loader.summary
      this.meta    = loader.meta

      // next load meta
      loader.loadContent(zip)
      this.index      = loader.index
      this.types      = loader.typeList
      this.typeMap    = loader.typeMap
      this.podDoc     = loader.podDoc
      this.chapters   = loader.chapterList
      this.chapterMap = loader.chapterMap
      this.resList    = loader.resList
      this.resMap     = loader.resMap
      this.srcList    = loader.srcList
      this.srcMap     = loader.srcMap
    }
    finally zip .close
  }

  ** File the pod was loaded from
  const File file

  ** Simple name of the pod such as "sys".
  const Str name

  ** Version number for this pod.
  const Version version

  ** Summary string for the pod
  const Str summary

  ** Always return `name`.
  override Str toStr() { name }

  ** Get the meta name/value pairs for this pod.
  ** See [docLang]`docLang::Pods#meta`.
  const Str:Str meta

  ** Document which models the index page for this pod
  const DocPodIndex index

  ** List of the public, documented types in this pod.
  const DocType[] types

  ** Get all types (public, internal, nodoc, etc)
  DocType[] allTypes() { typeMap.vals }

  ** Get the build timestamp or null if not available
  DateTime? ts() { DateTime.fromStr((meta["build.ts"] ?: meta["build.time"]) ?: "", false) }

  ** Find a type by name.  If the type doesn't exist and checked
  ** is false then return null, otherwise throw UnknownTypeErr.
  DocType? type(Str typeName, Bool checked := true)
  {
    t := typeMap[typeName]
    if (t != null) return t
    if (checked) throw UnknownTypeErr("${this.name}::${typeName}")
    return null
  }
  private const Str:DocType typeMap

  ** If this pod has an associated pod.fandoc chapter
  const DocChapter? podDoc

  ** A *manual* pod is a pod with two or more fandoc chapters and no types.
  Bool isManual() { types.isEmpty && chapters.size >= 2 }

  ** If this is a manual like docLang, return list of chapters.
  const DocChapter[] chapters

  ** Find a chapter by name.  If the chapter doesn't exist and
  ** checked is false then return null, otherwise throw Err.
  DocChapter? chapter(Str chapterName, Bool checked := true)
  {
    c := chapterMap[chapterName]
    if (c != null) return c
    if (checked) throw Err("Unknown chapter: ${this.name}::${chapterName}")
    return null
  }
  private const Str:DocChapter chapterMap

  ** Resource files in pod which are used to support the
  ** documentation such as images used by the fandoc chapters.
  ** Resources can only be located in doc/ sub-directory.
  const DocRes[] resList

  ** Return resource for filename, or if not available
  ** return null/raise exception.  This filenames is
  ** always relative to doc/ sub-directory.
  DocRes? res(Str filename, Bool checked := true)
  {
    uri := resMap[filename]
    if (uri != null) return uri
    if (checked) throw UnknownDocErr("resource file: $filename")
    return null
  }
  private const Str:DocRes resMap

  ** Source files in pod which should be included in documentation.
  const DocSrc[] srcList

  ** Return source code for filename, or if not
  ** available return null/raise exception.
  DocSrc? src(Str filename, Bool checked := true)
  {
    uri := srcMap[filename]
    if (uri != null) return uri
    if (checked) throw UnknownDocErr("source file: $filename")
    return null
  }
  private const Str:DocSrc srcMap

  ** Space name is same as `name`
  override Str spaceName() { name }

  **
  ** Find the document with the given name.  If not found raise
  ** UnknownDocErr or return null based on checked flag.
  ** The document namespace of a pod is:
  **   - "index": the DocPodIndex
  **   - "{type name}": DocType
  **   - "{chapter name}": DocChapter
  **   - "{filename}": DocRes
  **   - "src-{filename}": DocSrc
  **
  override Doc? doc(Str name, Bool checked := true)
  {
    // index
    if (name == "index") return index

    // type
    type := type(name, false)
    if (type != null) return type

    // chapter
    chapter := chapter(name, false)
    if (chapter != null) return chapter

    // source
    if (name.startsWith("src-"))
    {
      src := src(name[4..-1], false)
      if (src != null) return src
    }

    // resource
    res := res(name, false)
    if (res != null) return res

    // not found
    if (checked) throw UnknownDocErr("${this.name}::${name}")
    return null
  }

  override Void eachDoc(|Doc| f)
  {
    f(index)
    types.each(f)
    chapters.each(f)
    srcList.each(f)
    resList.each(f)
  }
}

**************************************************************************
** DocPodIndex
**************************************************************************

**
** DocPodIndex represents the index document of a DocPod.
**
const class DocPodIndex : Doc
{
  ** Constructor
  internal new make(DocPod pod, Obj[] toc)
  {
    this.pod = pod
    this.toc = toc
  }

  ** Parent pod
  const DocPod pod

  ** If this a API pod, this is the Str/DocType where the string indicates
  ** groupings such as "Classes", "Mixins", etc.  If this is a manual return
  ** the list of Str/DocChapter where Str indicates index grouping headers.
  const Obj[] toc

  ** The space for this doc is `pod`
  override DocSpace space() { pod }

  ** The document name under space is "index"
  override Str docName() { "index" }

  ** Title is pod name
  override Str title() { pod.name }

  ** Return true
  override Bool isSpaceIndex() { true }

  ** Default renderer is `DocPodIndexRenderer`
  override Type renderer() { DocPodIndexRenderer# }

  ** Index the type summary and all slot docs
  override Void onCrawl(DocCrawler crawler)
  {
    crawler.addKeyword(pod.name, pod.name, DocFandoc(DocLoc(pod.name, 0), pod.summary), null)
  }
}

**************************************************************************
** DocPodLoader
**************************************************************************

internal class DocPodLoader
{
  new make(File file, DocPod pod, |DocErr| onErr)
  {
    this.file  = file
    this.pod   = pod
    this.onErr = onErr
  }

  Void loadMeta(Zip zip)
  {
    // first read meta
    metaFile := zip.contents[`/meta.props`] ?: throw Err("Pod missing meta.props: $file")
    this.meta    = metaFile.readProps
    this.name    = getMeta("pod.name")
    this.summary = getMeta("pod.summary")
    this.version = Version.fromStr(getMeta("pod.version"))
  }

  private Str getMeta(Str n)
  {
    meta[n] ?: throw Err("Missing '$n' in meta.props")
  }

  Void loadContent(Zip zip)
  {
    // these are the data structures we'll be building up
    types     := Str:DocType[:]
    chapters  := Str:DocChapter[:]
    indexFog  := null
    resources := Uri[,]
    sources   := Uri[,]

    // iterate thru the zip file looking for the files we need
    zip.contents.each |f|
    {
      try
      {
        // if this is src/{file}, save to source list
        if (f.path[0] == "src")
        {
          if (f.path.size == 2) sources.add(f.uri)
          return
        }

        // we only care about files in doc/*
        if (f.path[0] != "doc") return

        // if doc/{type}.apidoc
        if (f.ext == "apidoc")
        {
          type := ApiDocParser(pod, f.in).parseType
          types[type.name] = type
          return
        }

        // if doc/{type}.fandoc
        if (f.ext == "fandoc")
        {
          chapter := DocChapter(this, f)
          chapters[chapter.name] = chapter
          return
        }

        // if doc/index.fog
        if (f.name == "index.fog")
        {
          indexFog = f.readObj
          return
        }

        // otherwise assume its a resource
        resources.add(f.uri)
      }
      catch (Err e) err("Cannot parse", DocLoc("${name}::${f}", 0), e)
    }

    // finish
    finishTypes(types)
    finishChapters(chapters, indexFog)
    finishResources(resources)
    finishSources(sources)
    finishIndex
  }

  private Void finishTypes(Str:DocType map)
  {
    // create sorted list
    list := map.vals.sort|a, b| { a.name <=> b.name }

    // filter out types which shouldn't be documented,
    // but leave them in the map for lookup
    list = list.exclude |t|
    {
      t.isNoDoc ||
      DocFlags.isInternal(t.flags) ||
      DocFlags.isSynthetic(t.flags)
    }

    // build toc
    toc := Obj[,]
    mixins  := DocType[,]
    classes := DocType[,]
    enums   := DocType[,]
    facets  := DocType[,]
    errs    := DocType[,]
    list.each |t|
    {
      if (t.isEnum) enums.add(t)
      else if (t.isFacet) facets.add(t)
      else if (t.isMixin) mixins.add(t)
      else if (t.isErr) errs.add(t)
      else classes.add(t)
    }
    if (mixins.size  > 0) toc.add("Mixins").addAll(mixins)
    if (classes.size > 0) toc.add("Classes").addAll(classes)
    if (enums.size   > 0) toc.add("Enums").addAll(enums)
    if (facets.size  > 0) toc.add("Facets").addAll(facets)
    if (errs.size    > 0) toc.add("Errs").addAll(errs)

    // save to fields
    this.typeMap  = map.toImmutable
    this.typeList = list.toImmutable
    this.toc      = toc.toImmutable
  }

  private Void finishChapters(Str:DocChapter map, Obj[]? indexFog)
  {
    // create sorted list of chapters
    list := map.vals.sort |a, b| { a.name <=> b.name }

    // if this pod has types, it can't be a manual
    if (!typeList.isEmpty || list.size <= 1)
    {
      this.podDoc = list.find |x| { x.isPodDoc }
      this.chapterList = this.podDoc == null ? DocChapter#.emptyList : DocChapter[podDoc]
      this.chapterMap  = Str:DocChapter[:].setList(this.chapterList) |x| { x.name }
      return
    }

    // generate indexFog if not specified
    if (indexFog == null)
    {
      if (!map.isEmpty) err("Manual missing '${name}::index.fog'", DocLoc(name, 0))
      indexFog = [,]
      list.each |c| { indexFog.add([c.name.toUri, ""]) }
    }

    // order the chapters by indexFog:
    //   - map DocChapter summary
    //   - check that chapters/index.fog match
    toc := Obj[,]
    indexLoc := DocLoc("${name}::index.fog", 0)
    indexMap := map.dup
    indexFog.each |item|
    {
      // grouping header
      if (item is Str) { toc.add(item); return }

      // get item as Uri/Str pair
      Uri? uri
      Str? summary
      try
      {
        uri = ((List)item).get(0)
        summary = ((List)item).get(1)
      }
      catch { err("Invalid item: $item", indexLoc); return }

      // lookup chapter and remove from map so we know it was indexed
      c := indexMap.remove(uri.toStr)
      if (c == null) { err("Unknown chapter: $uri", indexLoc); return }

      // add it toc
      toc.add(c)

      // map summary
      c.summaryRef.val = summary
    }

    // report errors for chapters not in index
    indexMap.each |c| { err("Chapter not in index: $c.name", indexLoc) }

    // redo list now that we have chapters ordered by index
    list = toc.findType(DocChapter#)

    // map DocChapter num/prev/next
    list.each |c, i|
    {
      c.numRef.val = i+1
      if (i > 0) c.prevRef.val = list[i-1]
      c.nextRef.val = list.getSafe(i+1)
    }

    // save to fields
    this.chapterMap  = map.toImmutable
    this.chapterList = list.toImmutable
    this.toc         = toc.toImmutable
  }

  private Void finishResources(Uri[] uris)
  {
    DocRes[] list := uris.sort.map |uri->DocRes| { DocRes(pod, uri) }
    this.resList = list.toImmutable
    this.resMap  = Str:DocRes[:].addList(list) |res| { res.uri.name }.toImmutable
  }

  private Void finishSources(Uri[] uris)
  {
    DocSrc[] list := uris.sort.map |uri->DocSrc| { DocSrc(pod, uri) }
    this.srcList = list.toImmutable
    this.srcMap  = Str:DocSrc[:].addList(list) |src| { src.uri.name }.toImmutable
  }

  private Void finishIndex()
  {
    this.index = DocPodIndex(pod, toc)
  }

  Void err(Str msg, DocLoc loc, Err? cause := null)
  {
    onErr(DocErr(msg, loc, cause))
  }

  File file                     // ctor
  DocPod pod                    // ctor
  |DocErr| onErr                // ctor
  [Str:Str]? meta               // load
  Str? name                     // loadMeta
  Str? summary                  // loadMeta
  Version? version              // loadMeta
  DocType[]? typeList           // finishTypes
  [Str:DocType]? typeMap        // finishTypes
  DocChapter[]? chapterList     // finishChapters
  [Str:DocChapter]? chapterMap  // finishChapters
  DocChapter? podDoc            // finishChapters
  DocRes[]? resList             // finishResource
  [Str:DocRes]? resMap          // finishResource
  DocSrc[]? srcList             // finishSource
  [Str:DocSrc]? srcMap          // finishSource
  Obj[]? toc                    // finishTypes/finishChapters
  DocPodIndex? index            // finishIndex
}