'use strict'

var st = require('st')
  , http = require('http')
  , path = require('path')
  , url = require('url')
  , fs = require('fs')
  , os = require('os')
  , js = require('atomify-js')
  , css = require('atomify-css')
  , prettyError = require('prettify-error')
  , ansiStyleFormat = require('style-format')
  , cssFiles = require('atomify-css/css')
  , lessFiles = require('atomify-css/less')
  , open = require('open')
  , browserSync = require('browser-sync').create()
  , htmlifyError = require('./htmlifyError.js')
  , defaultBundlePath = path.join(__dirname, '_bundle.js')
  , JS_REPLACE = '__ATOMIFY_JS__'
  , CSS_REPLACE = '__ATOMIFY_CSS__'
  , HTML_HEAD = '<!doctype html><html><head>'
    + '<meta charset="utf-8">'
    + '<meta http-equiv="X-UA-Compatible" content="IE=edge">'
    + '<meta name="viewport" content="initial-scale=1,width=device-width,user-scalable=0">'
  , cwd = process.cwd()
  , internals = {}
  , baseUrl
  , bs

internals.getDefaultHTML = function getDefaultHTML (args) {
  var src = HTML_HEAD
  src += '<title>generated by atomify</title>'
  if (args.css.entry) src += '<link rel="stylesheet" href="CSS">'
  src += '</head><body>'
  src += '<script src="JS"></script>'
  src += '</body></html>'

  src = src.replace('JS', args.js.alias)
  src = src.replace('CSS', args.css.alias)

  return src
}

internals.customDefaultHTML = ''

internals.setCustomDefaultHTML = function setCustomDefaultHTML (file, args) {
  var html = fs.readFileSync(file, {encoding: 'utf8'})
    , shouldReplaceJS = html.indexOf(JS_REPLACE) > -1
    , shouldReplaceCSS = html.indexOf(CSS_REPLACE) > -1

  if (shouldReplaceCSS) html = html.replace(CSS_REPLACE, args.css.alias)
  else html += '<link rel="stylesheet" href="' + args.css.alias + '">'

  if (shouldReplaceJS) html = html.replace(JS_REPLACE, args.js.alias)
  else html += '<script src="' + args.js.alias + '"></script>'

  internals.customDefaultHTML = html

  return internals.customDefaultHTML
}

internals.getCustomDefaultHTML = function getCustomDefaultHTML (req, args, callback) {
  var html = args.server.html
    , htmlIsFile = typeof html === 'string'

  // bail early if there is no html option
  if (!html) return void callback(null, null)

  // if the html arg is a string, assume it's a file name
  // if we've already cached this file, don't go read it again
  if (htmlIsFile && html !== internals.customDefaultHTML) {
    callback(null, internals.setCustomDefaultHTML(html, args))
  }
  else if (htmlIsFile){
    callback(null, internals.customDefaultHTML)
  }
  // if the html arg isn't a string, it should be a function that returns html
  else {
    html({js: args.js.alias, css: args.css.alias, request: req.url}, callback)
  }
}

internals.logError = function logError (err) {
  var prettyifiedErr = prettyError(err)

  // if any of our custom error parsing fails, don't die
  try {
    // babel errors are really special, so give them custom error handling
    if (err._babel) {
      bs.logger.error(ansiStyleFormat('{red}' + err.toString() + '{reset}'))
      console.error(err.codeFrame)
    }
    else {
      bs.logger.error(prettyifiedErr || err)
    }
  }
  catch (e) {
    console.error(err)
  }
}

internals.sendError = function sendError (err, res) {
  if (!res.headersSent) {
    res.setHeader('Content-Type', 'text/html')
    res.statusCode = 500
  }

  internals.logError(err)

  try {
    res.end(htmlifyError(err, {head: true}))
  }
  catch (e){
    res.end('<pre>' + err.toString() + err.stack + '</pre>')
  }

  return true
}

internals.serveDefaultPage = function serveDefaultPage (req, res, args) {
  internals.getCustomDefaultHTML(req, args, function gotCustomDefaultHTML (err, html) {
    var src = html || internals.getDefaultHTML(args)

    res.setHeader('Content-Type', 'text/html')

    if (err) internals.sendError(err, res)
    else res.end(src)
  })
}

internals.parseSyncOptions = function parseSyncOptions (opts) {
  if (!opts) return false
  if (typeof opts === 'object') return opts

  return {
    clicks: true
    , location: true
    , forms: true
    , scroll: true
  }
}

internals.getNormalizedPath = function getNormalizedPath (pathStr) {
  if (!pathStr || pathStr.charAt(0) === '/') return pathStr

  return '/' + pathStr
}

internals.responder = function responder (type, res) {
  return function response (err, src) {
    if (err) return void internals.sendError(err, res)

    if (!res.headersSent) res.setHeader('Content-Type', 'text/' + type)
    res.end(src || err)
  }
}

internals.startFileWatch = function startFileWatch (lr, args) {
  var watchedFiles = []
    , addFile = function addFile (file) {
      if (watchedFiles.indexOf(file) < 0){
        watchedFiles.push(file)
        browserSync.watch(file).on('change', function onCSSChanged () {
          bs.logger.info('{cyan:CSS changed: {magenta:%s', path.relative(cwd, file))
          browserSync.notify('CSS changed')
          browserSync.reload(args.css.alias)
        })
      }
    }

  if (!lr.quiet) {
    js.emitter.on('changed', function onJsChanged (filepath) {
      bs.logger.info('{cyan:JS changed: {magenta:%s', path.relative(cwd, filepath))
      browserSync.notify('Compiling, please wait!')
    })
  }

  js.emitter.on('bundle', function onJSBundled (time) {
    if (!lr.quiet) {
      bs.logger.info('{grey:Bundle updated in {magenta:%s', time + 'ms')
    }
    // for JS, just reload the whole page
    browserSync.reload()
  })

  cssFiles.emitter.on('file', addFile)
  lessFiles.emitter.on('file', addFile)
  addFile(args.css.entry)
}

module.exports = function server (args) {
  // sometimes the hostname has a '.local' on it, sometimes it doesn't
  var hostname = args.server.hostname || args.server.h ? os.hostname().replace(/\.local$/, '') + '.local' : 'localhost'

  args.server.url = args.server.url || 'http://' + hostname + ':1337/default'

  var stOptions = args.server.st || {path: cwd, cache: false}
    , mount = st(stOptions)
    , parsedUrl = url.parse(args.server.url)
    , port = args.server.port || parsedUrl.port
    , serverPath = internals.getNormalizedPath(args.server.path || parsedUrl.path)
    , lr = args.server.lr || args.server.l
    , launch = args.server.open || args.server.o
    , handler = args.server.handler || Function()
    , spaMode = args.server.spaMode

  if (args.server.sync || args.server.s) lr = {sync: true}

  args.servePath = stOptions.path

  // the router checks these, so they have to exist
  args.js = args.js || {}
  args.js.alias = internals.getNormalizedPath(args.js.alias || args.js.entry)
  args.css = args.css || {}
  args.css.alias = internals.getNormalizedPath(args.css.alias || args.css.entry)

  baseUrl = parsedUrl.protocol + '//' + parsedUrl.hostname

  // cache the browserify and watchify instances
  js.emitter.on('browserify', function storeBrowserifyInstace (b) {
    internals.browserify = b
  })
  js.emitter.on('watchify', function storeWatchifyInstance (w) {
    internals.watchify = w
  })

  if (lr) {
    // if live reloading, use watchify
    args.js.watch = true
    args.js.output = args.js.output || defaultBundlePath

    lr = typeof lr === 'boolean' ? {} : lr
    lr.patterns = lr.patterns
      ? lr.patterns
      : [path.join(args.servePath, '**/*.html')]

    if (args.css.output) lr.patterns.push(args.css.output)

    lr.port = lr.port || 3000
    lr.sync = internals.parseSyncOptions(lr.sync || lr.s)

    if (port >= lr.port && port <= lr.port + 2) {
      throw new Error('Ports ' + lr.port + ' through ' + (lr.port + 2) + 'in use for Live Reload')
    }

    bs = browserSync.init({
      proxy: {
        target: parsedUrl.hostname + ':' + port
      }
      , port: lr.port
      , injectChanges: true
      , ghostMode: lr.sync
      , notify: !!lr.quiet
      , logSnippet: false
      , open: false
      , logLevel: lr.verbose ? 'debug' : 'info'
      , logFileChanges: !!lr.quiet
      , logConnections: !!lr.quiet
      // don't minify for speed!
      , minify: false
      , startPath: '/default'
      , logPrefix: 'atomify'
      , files: lr.patterns.map(function doNotWatchNodeModules (match) {
        return {
          match: match
          , options: {
            ignored: /node_modules/
          }
        }
      })
    })

    internals.startFileWatch(lr, args)

    // listen for initial bundle completion before opening
    js(args.js, function onJSError (err) {
      if (err) internals.logError(err)
    }).on('end', function onJSBundled () {
      if (launch && bs.options && bs.options.port){
        open(baseUrl + ':' + bs.options.port + serverPath)
      }
    })
  }

  http.createServer(function createServer (req, res) {
    switch (req.url.split('?')[0]) {
      case args.js.alias || args.js.entry:
        if (lr) {
          res.setHeader('Content-Type', 'text/javascript')
          res.end(fs.readFileSync(args.js.output, {encoding: 'utf8'}))
        }
        else {
          js(args.js, internals.responder('javascript', res))
        }
      break

      case args.css.alias || args.css.entry:
        css(args.css, internals.responder('css', res))
      break

      case '/default':
        internals.serveDefaultPage(req, res, args)
      break

      default:
        if (spaMode) internals.serveDefaultPage(req, res, args)
        else if (handler(req, res, args)) return null
        else mount(req, res)
      break
    }
  }).listen(port)

  // if live reload is enabled we open after initial bundling
  if (launch && !lr) open(baseUrl + ':' + port + serverPath)
}

