#!/usr/bin/env node
// vim: set ts=2 sw=2 et tw=80:

const http       = require('http');
const url	       = require('url');
const fs	       = require('fs');
const formidable = require('formidable');

const routes = Object.create(null);

function noSubPaths(req, path) {
  const p = url.parse(req.url);
  if (p.pathname != path && p.pathname != path) {
    error(res, 404, `Only "${path}" path is accessible under this prefix`);
    return true;
  } else {
    return false;
  }
}

function error(res, code, text = '') {
  res.writeHead(code, { 'Content-Type': 'text/html' });
  res.end(`<!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset='utf-8'>
      <title>Error ${code}</title>
    </head>
    <body style="text-align: center">
      <h1>Error ${code}</h1>
      <p>${text}</p>
      <hr>
      <p>SA3 - HW4</p>
    </body>
  </html>`);
}

function fileData(reqUrl, prefix, options = {}) {
  const uri = decodeURIComponent(
    !options.isPath ? url.parse(reqUrl).pathname.substring(prefix.length) : reqUrl
  ).replace(/\/+$/, '');
  const file = __dirname + '/NodeStaticFiles' + uri;
  const name = file.substring(file.lastIndexOf('/') + 1);
  const ext = !options.noExt ? name.substring(name.indexOf('.') + 1) : null;

  return {
    uri: !uri ? '/' : uri,
    file: file,
    name: name,
    ext: ext
  };
}

routes['explore'] = (req, res) => {
  if (req.method != 'GET') {
    error(res, 405, 'Use this URL with only GET requests');
    return;
  }

  const { uri, file, name } = fileData(req.url, '/explore', { noExt: true });

  fs.readdir(file, { withFileTypes: true }, (err, dir) => {
    if (err) {
      error(res, 404, 'Directory not found');
      return;
    }

    const list = [{
      name: '.',
      path: '/explore' + (uri == '/' ? '' : uri) + '/'
    }];

    if (uri != '/') {
      const parentUri = uri.substring(0, uri.length - name.length - 1);
      list.push({ name: '..', path: '/explore' + parentUri });
    }
    for (const e of dir) {
      list.push({
        name: e.name,
        dir: !e.isFile(),
        path: (e.isFile() ? '/file' : '/explore') + uri +
        (uri == '/' ? '' : '/') + e.name
      });
    }

    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.write(`<!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>${uri} [DIR]</title>
      </head>
      <body>
        <h1>${uri} [DIR]</h1>
        <ul>`);
    for (const { name, dir, path } of list) {
      res.write(`<li>
        <a href="${path}">${name}${dir ? ' [DIR]' : ''}</a>
      </li>`);
    }
    res.end(`</ul>
      </body>
    </html>`);
  });
};

routes['file'] = (req, res) => {
  const FILE_TYPES = {
    html: 'text/html',
    css: 'text/css',
    txt: 'text/plain',
    mp4: 'video/mp4',
    ogv: 'video/ogg',
    gif: 'image/gif',
    jpg: 'image/jpeg',
    jpeg: 'image/jpeg',
    png: 'image/png',
    mp3: 'audio/mpeg',
    js: 'application/javascript',
    json: 'application/json',
    pdf: 'application/pdf',
    zip: 'application/zip'
  };

  if (req.method != 'GET') {
    error(res, 405, 'Use this URL with only GET requests');
    return;
  }

  const { file, name, ext } = fileData(req.url, '/file');

  fs.readFile(file, (err, data) => {
    if (err) {
      error(res, 404, 'File not found: ' + JSON.stringify(err));
      return;
    }

    res.setHeader('Content-Disposition', 'attachment; filename="' + name + '"');
    res.writeHead(200, { 'Content-Type': ext in FILE_TYPES ? FILE_TYPES[ext] :
      'application/octet-stream' });
    res.end(data);
  });
}

routes['upload'] = (req, res) => {
  if (req.method != 'GET' && req.method != 'POST') {
    error(res, 405, 'Use this URL with only GET or POST requests');
    return;
  }

  if (req.method == 'POST') {
    const form = new formidable.IncomingForm();
    form.uploadDir = __dirname + '/NodeStaticFiles';
    form.keepExtensions = true;

    form.parse(req);

    form.on('fileBegin', (name, file) => {
      file.path = file.path.substring(0, file.path.lastIndexOf('/') + 1);
      file.path += file.name;
    });
    form.on('end', () => {
      res.writeHead(302, { 'Location': '/explore' });
      res.end();
    });

    return;
  }

  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.end(`<!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Upload</title>
    </head>
    <body>
      <h1>Upload</h1>
      <form id="upload-form" enctype="multipart/form-data" method="post">
        <label for="file">File:</label>
        <input type="file" name="file"><br>
        <input type="submit" name="submit" value="Submit">
      </form>
    </body>
  </html>`);
};

function wordMap(string) {
  const words = string.trim().split(/[^\w]+/);
  const wordDict = {};
  for (const word of words) {
    if (word.trim() == '') {
      continue;
    }
    if (!wordDict[word]) {
      wordDict[word] = 1;
    } else {
      wordDict[word]++;
    }
  }
  return wordDict;
}

function getFileStats(req, res, path, callback) {
  const p = url.parse(req.url, true);

  const reqFile = p && p.query && '/' + p.query['file'];
  if (!reqFile) {
    error(res, 400, 'Search parameter "file" is required');
    return;
  }

  const { file, name, ext } = fileData(reqFile, path, { isPath: true });
  if (ext != 'txt' && ext != 'html') {
    error(res, 400, `Only txt and html files can be processed by ${path}`);
    return;
  }

  fs.readFile(file, 'utf8', (err, data) => {
    if (err) {
      error(res, 404, 'File not found: ' + JSON.stringify(err));
      return;
    }

    const dict = wordMap(data);

    callback({ reqFile: reqFile, dict: dict });
  });
}


routes['stats'] = (req, res) => {
  if (noSubPaths(req, '/stats')) {
    return;
  }

  getFileStats(req, res, '/stats', ({ reqFile, dict }) => {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.write(`<!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>Stats for ${reqFile}</title>
        <style>
          table { border-collapse: collapse; }
          td, th { border: 1px solid black; }
          th { font-weight: bold; }
        </style>
      </head>
      <body>
        <h1>Word frequency count for "${reqFile}"</h1>
        <table id="frequency-tbl">
          <thead>
            <tr><th>Word</th><th>Freq.</th></tr>
          </thead>
          <tbody>`);

    for (const word in dict) {
      res.write(`<tr>
              <td class="word">${word}</td>
              <td class="frequency">${dict[word]}</td>
            </tr>`);
    }

    res.end(`</tbody>
        </table>
      </body>
    </html>`);
  });
};

routes['cloud'] = (req, res) => {
  if (noSubPaths(req, '/cloud')) {
    return;
  }

  getFileStats(req, res, '/cloud', ({ reqFile, dict }) => {
    if (req.headers.accept && req.headers.accept == 'application/json') {
      res.writeHead(200, {'Content-Type': 'application/json' });
      res.end(JSON.stringify(dict, null, 2));
    } else {
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.write(`<!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="utf-8">
          <title>Cloud for ${reqFile}</title>
          <style>
            table { border-collapse: collapse; }
            td, th { border: 1px solid black; }
            th { font-weight: bold; }
          </style>
        </head>
        <body>
          <h1>Tag cloud for "${reqFile}"</h1>
          <div class="cloud">`);

      const max = Math.max(...Object.values(dict));
      const min = Math.min(...Object.values(dict));

      for (const word in dict) {
        const size = 0.75 + ((dict[word] - min) / max) * 4 + 'rem';
        res.write(`<span style="font-size: ${size}"> ${word} </span>`);
      }

      res.end(`</div>
        </body>
      </html>`);
    }
  });
};

// Main server handler
function onRequest(req, res) {
  const pathname = url.parse(req.url).pathname;
  const uri = pathname.split('/', 3)[1];

  if (typeof routes[uri] === 'function') {
    routes[uri](req, res);
  } else {
    error(res, 404, 'Path not found');
  }
}

http.createServer(onRequest).listen(3000);
console.log('Server started at localhost:3000');
