//
// Copyright (c) 2011, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   11 Aug 11  Brian Frank  Creation
//

using web
using fandoc::HtmlDocWriter

**
** DocEnv is the centralized glue class for managing documentation
** modeling and rendering:
**   - hooks for lookup and loading of spaces/pods
**   - hooks for theming HTML chrome and navigation
**   - hooks for renderering HTML pages
**   - hooks for hyperlink resolution
**
abstract const class DocEnv
{
  **
  ** Theme is responsible for the common chrome, styling, and
  ** navigation during rendering
  **
  virtual DocTheme theme() { DocTheme() }

  **
  ** Get the document which represents top level index.
  **
  virtual DocTopIndex topIndex() { DocTopIndex() }

  **
  ** Lookup a space by its space name.  If not found then return
  ** null or raise UnknownDocErr.  This method is called frequently
  ** during document rendering and linking so caching is expected.
  **
  abstract DocSpace? space(Str name, Bool checked := true)

  **
  ** Lookup a document by is spaceName and docName within that
  ** space.  If not found then return null or raise UnknownDocErr.
  ** Default implementation delegates to `space` and `DocSpace.doc`.
  **
  virtual Doc? doc(Str spaceName, Str docName, Bool checked := true)
  {
    doc := space(spaceName, false)?.doc(docName, false)
    if (doc != null) return doc
    if (checked) throw UnknownDocErr("$spaceName::$docName")
    return null
  }

  **
  ** Render the given document to the specified output stream.
  ** Default implementation uses `Doc.renderer`.
  **
  virtual Void render(WebOutStream out, Doc doc)
  {
    DocRenderer r := doc.renderer.make([this, out, doc])
    r.writeDoc
  }

  **
  ** Return URI used to link the from doc to the target doc.
  ** Also see `linkUriExt`.
  **
  virtual Uri linkUri(DocLink link)
  {
    if (link.absUri != null) return link.absUri
    s := StrBuf()
    if (link.from.isTopIndex)
    {
      if (!link.target.isTopIndex)
        s.add(link.target.space.spaceName).add("/")
    }
    else if (link.target.isTopIndex)
    {
      s.add("../")
    }
    else if (link.from.space !== link.target.space)
    {
      s.add("../").add(link.target.space.spaceName).add("/")
    }
    docName := link.target.docName
    if (docName == "pod-doc") docName = "index"
    s.add(docName)
    ext := linkUriExt
    if (ext != null) s.add(ext)
    if (link.frag != null) s.add("#").add(link.frag)
    return s.toStr.toUri
  }

  **
  ** Return the file extension (including the dot) to
  ** suffix all link URIs.  Default returns ".html"
  **
  virtual Str? linkUriExt() { ".html"}

  **
  ** Resolve the link relative to the given from document.
  ** See `DocLink` for the built-in formats.
  **
  virtual DocLink? link(Doc from, Str link, Bool checked := true)
  {
    // if absolute spaceName::docName
    colons := link.index("::")
    space := from.space as DocSpace
    docName := link
    if (colons != null)
    {
      spaceName := link[0..<colons]
      docName   = link[colons+2..-1]
      space     = this.space(spaceName, checked)
      if (space == null) return null
    }

    // check if we have a Type.slot
    dot := docName.index(".")
    if (dot != null)
    {
      typeName := docName[0..<dot]
      slotName := docName[dot+1..-1]
      type := space.doc(typeName, false) as DocType
      if (type != null)
      {
        slot := type.slot(slotName)
        if (slot != null) return DocLink(from, type, "${typeName}.${slotName}", slotName)
      }
    }

    // check for slot in Type
    if (from is DocType)
    {
      slot := ((DocType)from).slot(docName, false)
      if (slot != null) return DocLink(from, from, docName, docName)
    }

    // check if we have Chatper#frag
    pound := docName.index("#")
    if (pound != null)
    {
      chapterName := docName[0..<pound]
      headingName := docName[pound+1..-1]
      doc := (chapterName.isEmpty ? from : space.doc(chapterName, false))
      if (doc != null)
      {
        heading := doc.heading(headingName, false)
        if (heading != null) return DocLink(from, doc, doc.title, headingName)
      }
    }

    // check for document
    doc := space.doc(docName, false)
    if (doc != null)
    {
      if (doc is DocType) return DocLink(from, doc, doc.docName)
      return DocLink(from, doc, doc.title)
    }

    // no joy
    if (checked) throw Err("Broken link: $link")
    return null
  }

  **
  ** Hook to perform extra DocLink checking such as links to NoDocs
  **
  virtual Void linkCheck(DocLink link, DocLoc loc)
  {
    type := link.target as DocType
    if (type != null)
    {
      if (type.isNoDoc) errReport(DocErr("Link to NoDoc type $type.qname", loc))
      else if (link.frag != null)
      {
        slot := type.slot(link.frag, false)
        if (slot != null && slot.isNoDoc) errReport(DocErr("Link to NoDoc slot $slot.qname", loc))
      }
    }
  }

  ** Create HtmlDocWriter for rendering Fandoc
  @NoDoc
  virtual HtmlDocWriter initFandocHtmlWriter(OutStream out)
  {
    HtmlDocWriter(out)
  }


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

  virtual DocErr errReport(DocErr err)
  {
    echo("$err.loc: $err.msg")
    if (err.cause != null) err.cause.trace
    return err
  }
}