/*
 * Copyright 2014 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/* eslint-disable object-shorthand */

'use strict';

var http = require('http');
var path = require('path');
var fs = require('fs');

var mimeTypes = {
  '.css': 'text/css',
  '.html': 'text/html',
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.svg': 'image/svg+xml',
  '.pdf': 'application/pdf',
  '.xhtml': 'application/xhtml+xml',
  '.gif': 'image/gif',
  '.ico': 'image/x-icon',
  '.png': 'image/png',
  '.log': 'text/plain',
  '.bcmap': 'application/octet-stream',
  '.properties': 'text/plain',
};

var defaultMimeType = 'application/octet-stream';

function WebServer() {
  this.root = '.';
  this.host = 'localhost';
  this.port = 0;
  this.server = null;
  this.verbose = false;
  this.cacheExpirationTime = 0;
  this.disableRangeRequests = false;
  this.hooks = {
    'GET': [crossOriginHandler],
    'POST': [],
  };
}
WebServer.prototype = {
  start: function (callback) {
    this._ensureNonZeroPort();
    this.server = http.createServer(this._handler.bind(this));
    this.server.listen(this.port, this.host, callback);
    console.log(
      'Server running at http://' + this.host + ':' + this.port + '/');
  },
  stop: function (callback) {
    this.server.close(callback);
    this.server = null;
  },
  _ensureNonZeroPort: function () {
    if (!this.port) {
      // If port is 0, a random port will be chosen instead. Do not set a host
      // name to make sure that the port is synchronously set by .listen().
      var server = http.createServer().listen(0);
      var address = server.address();
      // .address().port being available synchronously is merely an
      // implementation detail. So we are defensive here and fall back to some
      // fixed port when the address is not available yet.
      this.port = address ? address.port : 8000;
      server.close();
    }
  },
  _handler: function (req, res) {
    var url = req.url.replace(/\/\//g, '/');
    var urlParts = /([^?]*)((?:\?(.*))?)/.exec(url);
    try {
      // Guard against directory traversal attacks such as
      // `/../../../../../../../etc/passwd`, which let you make GET requests
      // for files outside of `this.root`.
      var pathPart = path.normalize(decodeURI(urlParts[1]));
      // path.normalize returns a path on the basis of the current platform.
      // Windows paths cause issues in statFile and serverDirectoryIndex.
      // Converting to unix path would avoid platform checks in said functions.
      pathPart = pathPart.replace(/\\/g, '/');
    } catch (ex) {
      // If the URI cannot be decoded, a `URIError` is thrown. This happens for
      // malformed URIs such as `http://localhost:8888/%s%s` and should be
      // handled as a bad request.
      res.writeHead(400);
      res.end('Bad request', 'utf8');
      return;
    }
    var queryPart = urlParts[3];
    var verbose = this.verbose;

    var methodHooks = this.hooks[req.method];
    if (!methodHooks) {
      res.writeHead(405);
      res.end('Unsupported request method', 'utf8');
      return;
    }
    var handled = methodHooks.some(function (hook) {
      return hook(req, res);
    });
    if (handled) {
      return;
    }

    if (pathPart === '/favicon.ico') {
      fs.realpath(path.join(this.root, 'test/resources/favicon.ico'),
                  checkFile);
      return;
    }

    var disableRangeRequests = this.disableRangeRequests;
    var cacheExpirationTime = this.cacheExpirationTime;

    var filePath;
    fs.realpath(path.join(this.root, pathPart), checkFile);

    function checkFile(err, file) {
      if (err) {
        res.writeHead(404);
        res.end();
        if (verbose) {
          console.error(url + ': not found');
        }
        return;
      }
      filePath = file;
      fs.stat(filePath, statFile);
    }

    var fileSize;

    function statFile(err, stats) {
      if (err) {
        res.writeHead(500);
        res.end();
        return;
      }

      fileSize = stats.size;
      var isDir = stats.isDirectory();
      if (isDir && !/\/$/.test(pathPart)) {
        res.setHeader('Location', pathPart + '/' + urlParts[2]);
        res.writeHead(301);
        res.end('Redirected', 'utf8');
        return;
      }
      if (isDir) {
        serveDirectoryIndex(filePath);
        return;
      }

      var range = req.headers['range'];
      if (range && !disableRangeRequests) {
        var rangesMatches = /^bytes=(\d+)\-(\d+)?/.exec(range);
        if (!rangesMatches) {
          res.writeHead(501);
          res.end('Bad range', 'utf8');
          if (verbose) {
            console.error(url + ': bad range: "' + range + '"');
          }
          return;
        }
        var start = +rangesMatches[1];
        var end = +rangesMatches[2];
        if (verbose) {
          console.log(url + ': range ' + start + ' - ' + end);
        }
        serveRequestedFileRange(filePath,
                                start,
                                isNaN(end) ? fileSize : (end + 1));
        return;
      }
      if (verbose) {
        console.log(url);
      }
      serveRequestedFile(filePath);
    }

    function escapeHTML(untrusted) {
      // Escape untrusted input so that it can safely be used in a HTML response
      // in HTML and in HTML attributes.
      return untrusted
        .replace(/&/g, '&')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
    }

    function serveDirectoryIndex(dir) {
      res.setHeader('Content-Type', 'text/html');
      res.writeHead(200);

      if (queryPart === 'frame') {
        res.end('<html><frameset cols=*,200><frame name=pdf>' +
          '<frame src=\"' + encodeURI(pathPart) +
          '?side\"></frameset></html>', 'utf8');
        return;
      }
      var all = queryPart === 'all';
      fs.readdir(dir, function (err, files) {
        if (err) {
          res.end();
          return;
        }
        res.write('<html><head><meta charset=\"utf-8\"></head><body>' +
                  '<h1>PDFs of ' + pathPart + '</h1>\n');
        if (pathPart !== '/') {
          res.write('<a href=\"..\">..</a><br>\n');
        }
        files.forEach(function (file) {
          var stat;
          var item = pathPart + file;
          var href = '';
          var label = '';
          var extraAttributes = '';
          try {
            stat = fs.statSync(path.join(dir, file));
          } catch (e) {
            href = encodeURI(item);
            label = file + ' (' + e + ')';
            extraAttributes = ' style="color:red"';
          }
          if (stat) {
            if (stat.isDirectory()) {
              href = encodeURI(item);
              label = file;
            } else if (path.extname(file).toLowerCase() === '.pdf') {
              href = '/web/viewer.html?file=' + encodeURIComponent(item);
              label = file;
              extraAttributes = ' target="pdf"';
            } else if (all) {
              href = encodeURI(item);
              label = file;
            }
          }
          if (label) {
            res.write('<a href=\"' + escapeHTML(href) + '\"' +
              extraAttributes + '>' + escapeHTML(label) + '</a><br>\n');
          }
        });
        if (files.length === 0) {
          res.write('<p>no files found</p>\n');
        }
        if (!all && queryPart !== 'side') {
          res.write('<hr><p>(only PDF files are shown, ' +
            '<a href=\"?all\">show all</a>)</p>\n');
        }
        res.end('</body></html>');
      });
    }

    function serveRequestedFile(filePath) {
      var stream = fs.createReadStream(filePath, { flags: 'rs', });

      stream.on('error', function (error) {
        res.writeHead(500);
        res.end();
      });

      var ext = path.extname(filePath).toLowerCase();
      var contentType = mimeTypes[ext] || defaultMimeType;

      if (!disableRangeRequests) {
        res.setHeader('Accept-Ranges', 'bytes');
      }
      res.setHeader('Content-Type', contentType);
      res.setHeader('Content-Length', fileSize);
      if (cacheExpirationTime > 0) {
        var expireTime = new Date();
        expireTime.setSeconds(expireTime.getSeconds() + cacheExpirationTime);
        res.setHeader('Expires', expireTime.toUTCString());
      }
      res.writeHead(200);

      stream.pipe(res);
    }

    function serveRequestedFileRange(filePath, start, end) {
      var stream = fs.createReadStream(filePath, {
        flags: 'rs', start: start, end: end - 1, });

      stream.on('error', function (error) {
        res.writeHead(500);
        res.end();
      });

      var ext = path.extname(filePath).toLowerCase();
      var contentType = mimeTypes[ext] || defaultMimeType;

      res.setHeader('Accept-Ranges', 'bytes');
      res.setHeader('Content-Type', contentType);
      res.setHeader('Content-Length', (end - start));
      res.setHeader('Content-Range',
        'bytes ' + start + '-' + (end - 1) + '/' + fileSize);
      res.writeHead(206);

      stream.pipe(res);
    }

  },
};

// This supports the "Cross-origin" test in test/unit/api_spec.js
// It is here instead of test.js so that when the test will still complete as
// expected if the user does "gulp server" and then visits
// http://localhost:8888/test/unit/unit_test.html?spec=Cross-origin
function crossOriginHandler(req, res) {
  if (req.url === '/test/pdfs/basicapi.pdf?cors=withCredentials') {
    res.setHeader('Access-Control-Allow-Origin', req.headers['origin']);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }
  if (req.url === '/test/pdfs/basicapi.pdf?cors=withoutCredentials') {
    res.setHeader('Access-Control-Allow-Origin', req.headers['origin']);
  }
}

exports.WebServer = WebServer;