# **server.coffee** is the main guts of the express version
# of (Smallest Federated Wiki)[https://github.com/WardCunningham/Smallest-Federated-Wiki].
# The CLI and Farm are just front ends
# for setting arguments, and spawning servers.  In a complex system
# you would probably want to replace the CLI/Farm with your own code,
# and use server.coffee directly.
#
#### Dependencies ####
# anything not in the standard library is included in the repo, or
# can be installed with an:
#     npm install

require('coffee-trace')

# Standard lib
fs = require 'fs'
path = require 'path'
http = require 'http'
child_process = require 'child_process'
spawn = child_process.spawn

# From npm
mkdirp = require 'mkdirp'
express = require 'express'
hbs = require 'hbs'
glob = require 'glob'
es = require 'event-stream'
JSONStream = require 'JSONStream'
async = require 'async'
f = require('flates')


# Local files
random = require './random_id'
defargs = require './defaultargs'
wiki = require 'wiki-client/lib/wiki'
pluginsFactory = require './plugins'
Persona = require './persona_auth'

render = (page) ->
  return f.div({class: "twins"}, f.p('')) + '\n' +
  f.div({class: "header"}, f.h1(
    f.a({href: '/', style: 'text-decoration: none'},
      f.img({height: '32px', src: '/favicon.png'})) +
      ' ' + page.title)) + '\n' +
    f.div {class: "story"},
      page.story.map((story) ->
        if story.type is 'paragraph'
          f.div {class: "item paragraph"}, f.p(story.text)
        else if story.type is 'image'
          f.div {class: "item image"},
            f.img({class: "thumbnail", src: story.url}),
            f.p(story.text or story.caption or 'uploaded image')
        else f.div {class: "item error"}, f.p(story.type)
      ).join('\n')

# Set export objects for node and coffee to a function that generates a sfw server.
module.exports = exports = (argv) ->
  # Create the main application object, app.
  app = express()

  # defaultargs.coffee exports a function that takes the argv object
  # that is passed in and then does its
  # best to supply sane defaults for any arguments that are missing.
  argv = defargs(argv)

  app.startOpts = do ->
    options = {}
    for own k, v of argv
      options[k] = v
    options

  log = (stuff...) ->
    console.log stuff if argv.debug

  loga = (stuff...) ->
    console.log stuff


  errorHandler = (req, res, next) ->
    fired = false
    res.e = (error, status) ->
      if !fired
        fired = true
        res.statusCode = status or 500
        res.end 'Server ' + error
        log "Res sent:", res.statusCode, error
      else
        log "Allready fired", error
    next()

  # Require the database adapter and initialize it with options.
  app.pagehandler = pagehandler = require(argv.database.type)(argv)

  #### Setting up Authentication ####
  # The owner of a server is simply the open id url that the wiki
  # has been claimed with.  It is persisted at argv.status/open_id.identity,
  # and kept in memory as owner.  A falsy owner implies an unclaimed wiki.
  owner = ''

  # Attempt to figure out if the wiki is claimed or not,
  # if it is return the owner, if not set the owner
  # to the id if it is provided.
  setOwner = (id, cb) ->
    fs.exists argv.id, (exists) ->
      if exists
        fs.readFile(argv.id, (err, data) ->
          if err then return cb err
          owner += data
          cb())
      else if id
        fs.writeFile(argv.id, id, (err) ->
          if err then return cb err
          loga "Claimed by #{id}"
          owner = id
          cb())
      else
        cb()

  #### Middleware ####
  #
  # Allow json to be got cross origin.
  cors = (req, res, next) ->
    res.header('Access-Control-Allow-Origin', '*')
    next()


  remoteGet = (remote, slug, cb) ->
    [host, port] = remote.split(':')
    getopts = {
      host: host
      port: port or 80
      path: "/#{slug}.json"
    }
    # TODO: This needs more robust error handling, just trying to
    # keep it from taking down the server.
    http.get(getopts, (resp) ->
      responsedata = ''
      resp.on 'data', (chunk) ->
        responsedata += chunk

      resp.on 'error', (e) ->
        cb(e, 'Page not found', 404)

      resp.on 'end', ->
        if resp.statusCode == 404
          cb(null, 'Page not found', 404)
        else if responsedata
          cb(null, JSON.parse(responsedata), resp.statusCode)
        else
          cb(null, 'Page not found', 404)

    ).on 'error', (e) ->
      cb(e, 'Page not found', 404)

  persona = Persona(log, loga, argv)

  # Persona middleware needs access to this module's owner variable
  getOwner = ->
    owner

  #### Express configuration ####
  # Set up all the standard express server options,
  # including hbs to use handlebars/mustache templates
  # saved with a .html extension, and no layout.
  app.configure ->
    app.set('views', path.join(__dirname, '..', '/views'))
    app.set('view engine', 'html')
    app.engine('html', hbs.__express)
    app.set('view options', layout: false)

    # use logger, at least in development, probably needs a param to configure (or turn off).
    # use stream to direct to somewhere other than stdout.
    app.use(express.logger('tiny'))
    app.use(express.cookieParser())
    app.use(express.bodyParser())
    app.use(express.methodOverride())
    app.use(express.session({ secret: 'notsecret'}))
    app.use(persona.authenticate_session(getOwner))
    app.use(errorHandler)
    app.use(app.router)

    # Add static route to the client
    app.use(express.static(argv.client))

    # Add static routes to the plugins client.
    glob "wiki-plugin-*/client", {cwd: argv.packageDir}, (e, plugins) ->
      plugins.map (plugin) ->
        pluginName = plugin.slice(12, -7)
        pluginPath = '/plugins/' + pluginName
        app.use(pluginPath, express.static(path.join(argv.packageDir, plugin)))



  ##### Set up standard environments. #####
  # In dev mode turn on console.log debugging as well as showing the stack on err.
  app.configure 'development', ->
    app.use(express.errorHandler({ dumpExceptions: true, showStack: true }))
    argv.debug = console? and true

  # Show all of the options a server is using.
  log argv

  # Swallow errors when in production.
  app.configure 'production', ->
    app.use(express.errorHandler())

  # authenticated indicates that we have a logged in user.
  # The req.isAuthenticated returns true on an unclaimed wiki
  # so we must also check that we have a logged in user
  is_authenticated = (req) ->
    if req.isAuthenticated()
      if !! req.session.email
        return true
    return false

  #### Routes ####
  # Routes currently make up the bulk of the Express port of
  # Smallest Federated Wiki. Most routes use literal names,
  # or regexes to match, and then access req.params directly.

  ##### Redirects #####
  # Common redirects that may get used throughout the routes.
  index = argv.home + '.html'

  oops = '/oops'

  ##### Get routes #####
  # Routes have mostly been kept together by http verb, with the exception
  # of the openID related routes which are at the end together.

  # Main route for initial contact.  Allows us to
  # link into a specific set of pages, local and remote.
  # Can also be handled by the client, but it also sets up
  # the login status, and related footer html, which the client
  # relies on to know if it is logged in or not.
  app.get ///^((/[a-zA-Z0-9:.-]+/[a-z0-9-]+(_rev\d+)?)+)/?$///, (req, res) ->
    urlPages = (i for i in req.params[0].split('/') by 2)[1..]
    urlLocs = (j for j in req.params[0].split('/')[1..] by 2)
    info = {
      pages: []
      authenticated: is_authenticated(req)
      user: req.session.email
      ownedBy: if owner
        'Site owned by ' + owner.substr(0, owner.indexOf('@'))
      else
        ''
      loginStatus: if owner
        if req.isAuthenticated()
          'logout'
        else 'login'
      else 'claim'
      loginBtnTxt: if owner
        if req.isAuthenticated()
          'Sign out'
        else 'Sign in with your Email'
      else 'Claim with your Email'
    }
    for page, idx in urlPages
      if urlLocs[idx] is 'view'
        pageDiv = {page}
      else
        pageDiv = {page, origin: """data-site=#{urlLocs[idx]}"""}
      info.pages.push(pageDiv)
    res.render('static.html', info)

  app.get ///([a-z0-9-]+)\.html$///, (req, res, next) ->
    file = req.params[0]
    log(file)
    if file is 'runtests'
      return next()
    pagehandler.get file, (e, page, status) ->
      if e then return res.e e
      if status is 404
        return res.send page, status
      info = {
        pages: [
          page: file
          generated: """data-server-generated=true"""
          story: wiki.resolveLinks(render(page))
        ]
        user: req.session.email
        authenticated: is_authenticated(req)
        ownedBy: if owner
          'Site owned by ' + owner.substr(0, owner.indexOf('@'))
        else
          ''
        loginStatus: if owner
          if req.isAuthenticated()
            'logout'
          else 'login'
        else 'claim'
        loginBtnTxt: if owner
          if req.isAuthenticated()
            'Sign out'
          else 'Sign in with your Email'
        else 'Claim with your Email'
      }
      res.render('static.html', info)

  app.get ///system/factories.json///, (req, res) ->
    res.status(200)
    res.header('Content-Type', 'application/json')
# Plugins are located in packages in argv.packageDir, with package names of the form wiki-plugin-*
    glob path.join(argv.packageDir, 'wiki-plugin-*', 'factory.json'), (e, files) ->
      if e then return res.e(e)
      files = files.map (file) ->
        return fs.createReadStream(file).on('error', res.e).pipe(JSONStream.parse())

      es.concat.apply(null, files)
        .on('error', res.e)
        .pipe(JSONStream.stringify())
        .pipe(res)


  ###### Json Routes ######
  # Handle fetching local and remote json pages.
  # Local pages are handled by the pagehandler module.
  app.get ///^/([a-z0-9-]+)\.json$///, cors, (req, res) ->
    file = req.params[0]
    pagehandler.get file, (e, page, status) ->
      if e then return res.e e
      res.send(status or 200, page)

  # Remote pages use the http client to retrieve the page
  # and sends it to the client.  TODO: consider caching remote pages locally.
  app.get ///^/remote/([a-zA-Z0-9:\.-]+)/([a-z0-9-]+)\.json$///, (req, res) ->
    remoteGet req.params[0], req.params[1], (e, page, status) ->
      if e
        log "remoteGet error:", e
        return res.e e
      res.send(status or 200, page)

  ###### Favicon Routes ######
  # If favLoc doesn't exist send 404 and let the client
  # deal with it.
  favLoc = path.join(argv.status, 'favicon.png')
  app.get '/favicon.png', cors, (req,res) ->
    res.sendfile(favLoc)

  authenticated = (req, res, next) ->
    if req.isAuthenticated()
      next()
    else
      console.log 'rejecting', req.path
      res.send(403)

  # Accept favicon image posted to the server, and if it does not already exist
  # save it.
  app.post '/favicon.png', authenticated, (req, res) ->
    favicon = req.body.image.replace(///^data:image/png;base64,///, "")
    buf = new Buffer(favicon, 'base64')
    fs.exists argv.status, (exists) ->
      if exists
        fs.writeFile favLoc, buf, (e) ->
          if e then return res.e e
          res.send('Favicon Saved')

      else
        mkdirp argv.status, ->
          fs.writeFile favLoc, buf, (e) ->
            if e then return res.e e
            res.send('Favicon Saved')

  # Redirect remote favicons to the server they are needed from.
  app.get ///^/remote/([a-zA-Z0-9:\.-]+/favicon.png)$///, (req, res) ->
    remotefav = "http://#{req.params[0]}"

    res.redirect(remotefav)

  ###### Meta Routes ######
  # Send an array of pages in the database via json
  app.get '/system/slugs.json', cors, (req, res) ->
    fs.readdir argv.db, (e, files) ->
      if e then return res.e e
      res.send(files)

# Returns a list of installed plugins. (does this get called anymore!)
  app.get '/system/plugins.json', cors, (req, res) ->
    glob "wiki-plugin-*", {cwd: argv.packageDir}, (e, files) ->
      if e then return res.e e
      # extract the plugin name from the name of the directory it's installed in
      files = files.map (file) -> file.slice(12)
      res.send(files)


  app.get '/system/sitemap.json', cors, (req, res) ->
    pagehandler.pages (e, sitemap) ->
      return res.e(e) if e
      res.json(sitemap)


  app.post '/persona_login',
           cors,
           persona.verify_assertion(getOwner, setOwner)


  app.post '/persona_logout', cors, (req, res) ->
    req.session.destroy (err) ->
      res.send(err || "OK")

  ##### Put routes #####

  app.put /^\/page\/([a-z0-9-]+)\/action$/i, authenticated, (req, res) ->
    action = JSON.parse(req.body.action)
    # Handle all of the possible actions to be taken on a page,
    actionCB = (e, page, status) ->
      #if e then return res.e e
      if status is 404
        res.send(page, status)
      # Using Coffee-Scripts implicit returns we assign page.story to the
      # result of a list comprehension by way of a switch expression.
      try
        page.story = switch action.type
          when 'move'
            action.order.map (id) ->
              page.story.filter((para) ->
                id == para.id
              )[0] or throw('Ignoring move. Try reload.')

          when 'add'
            idx = page.story.map((para) -> para.id).indexOf(action.after) + 1
            page.story.splice(idx, 0, action.item)
            page.story

          when 'remove'
            page.story.filter (para) ->
              para?.id != action.id

          when 'edit'
            page.story.map (para) ->
              if para.id is action.id
                action.item
              else
                para


          when 'create', 'fork'
            page.story or []

          else
            log "Unfamiliar action:", action
            page.story
      catch e
        return res.e e

      # Add a blank journal if it does not exist.
      # And add what happened to the journal.
      if not page.journal
        page.journal = []
      if action.fork
        page.journal.push({type: "fork", site: action.fork})
        delete action.fork
      page.journal.push(action)
      pagehandler.put req.params[0], page, (e) ->
        if e then return res.e e
        res.send('ok')
        log 'saved'

    log action
    # If the action is a fork, get the page from the remote server,
    # otherwise ask pagehandler for it.
    if action.fork
      remoteGet(action.fork, req.params[0], actionCB)
    else if action.type is 'create'
      # Prevent attempt to write circular structure
      itemCopy = JSON.parse(JSON.stringify(action.item))
      pagehandler.get req.params[0], (e, page, status) ->
        if e then return actionCB(e)
        unless status is 404
          res.send('Page already exists.', 409)
        else
          actionCB(null, itemCopy)

    else if action.type == 'fork'
      if action.item # push
        itemCopy = JSON.parse(JSON.stringify(action.item))
        delete action.item
        actionCB(null, itemCopy)
      else # pull
        remoteGet(action.site, req.params[0], actionCB)
    else
      pagehandler.get(req.params[0], actionCB)

  # Return the oops page when login fails.
  app.get '/oops', (req, res) ->
    res.statusCode = 403
    res.render('oops.html', {msg:'This is not your wiki!'})

  # Traditional request to / redirects to index :)
  app.get '/', (req, res) ->
    res.redirect(index)


  #### Start the server ####
  # Wait to make sure owner is known before listening.
  setOwner null, (e) ->
    # Throw if you can't find the initial owner
    if e then throw e
    server = app.listen argv.port, argv.host, ->
      app.emit 'listening'
      loga "Smallest Federated Wiki server listening on", argv.port, "in mode:", app.settings.env
    ### Plugins ###
    # Should replace most WebSocketServers below.
    plugins = pluginsFactory(argv)
    plugins.startServers({server: server, argv})

  # Return app when called, so that it can be watched for events and shutdown with .close() externally.
  app
