/* Gopher.js - Backend for fetching, storing and manipulating data over the Gopher network.
  (c) 2020 Senri Software
  Requires an environment with mozTCPSocket. Primarily intended for KaiOS.
  This is pre-alpha quality software, and a "sneak peek" of what to come.
  I don't yet feel comfortable licensing Gopher.js.
*/

"use strict";

let defaultentry = {
   "type": "1",
   "name": "Gopher root",
   "path": "",
   "host": "192.168.1.44",
   "port": 70,
   "query": ""
 };

let emptyentry = {
   "type": "",
   "name": "",
   "path": "",
   "host": "",
   "port": 0,
   "query": ""
 };

let decoder = new TextDecoder();
let encoder = new TextEncoder();

function completeGopherEntry(entry) {
 /* Takes an incomplete GopherEntry and returns a new GopherEntry
    with unset values set to default. */
 let newentry = Object.assign({}, emptyentry, entry);
 for (let p in newentry) {
   if (!newentry[p]) {
     newentry[p] = defaultentry[p];
   };
 };
 return newentry;
};

function GopherSocket(entry) {
 /* Returns a GopherSocket connecting to the given GopherEntry. */
 entry = completeGopherEntry(entry);
 let datapackets = [];
 let obj = {
   "socket": navigator.mozTCPSocket.open(entry.host, entry.port, {binaryType: "arraybuffer"}),
   "entry": entry,
   "received": 0,
   "data": new Uint8Array(0)
 };
 obj.socket.ondata = function(event) {
   datapackets.push(event.data);
   obj.received += event.data.byteLength;
   console.log("%d bytes received", obj.received);
 };
 obj.socket.onopen = function() {
   console.log("Connected to %s:%d. Sending selector \"%s\" with query \"%s\"",
               entry.host, entry.port, entry.path, entry.query);
   let outbuffer
   if (entry.query) {
     outbuffer = encoder.encode(entry.path+"\t"+entry.query+"\r\n");
   } else {
     outbuffer = encoder.encode(entry.path+"\r\n");
   };
   obj.socket.send(outbuffer.buffer);
 };
 obj.socket.onclose = function() {
   console.log("Disconnected from %s:%d.", entry.host, entry.port);
   obj.data = new Uint8Array(obj.received);
   let acc = 0;
   datapackets.forEach( function(packet) {
     obj.data.set(new Uint8Array(packet), acc);
     acc += packet.byteLength;
   });
   datapackets = [];
   if (entry.type === "1" || entry.type === "7") {
     obj.dir = rawToGopherDirectory(obj.data);
   };
 };
 obj.socket.onerror = function() {
   console.error("A connection error occurred (%s:%d). Disconnecting...", entry.host, entry.port);
   obj.socket.close();
 };
 return obj;
};

function rawToGopherEntry(dirline) {
 /* Gets a single gopher entry as a raw string (trimming \r\n if necessary)
    and returns a GopherEntry. */
 dirline = dirline.trim();
 if (dirline.length < 2) {
   return null;
 };
 let direntry = dirline.split("\t");
 if (direntry.length < 4) {
   console.error("Malformed gopher entry (not enough fields specified): %o", direntry);
   return {
     "type": "3",
     "name": "(Client-side error: Malformed entry)",
     "path": "",
     "host": "error.invalid",
     "port": 1
   };
 };
 return {
   "type": direntry[0][0],
   "name": direntry[0].slice(1),
   "path": direntry[1],
   "host": direntry[2],
   "port": parseInt(direntry[3],10)
 };
};

function rawToGopherDirectory(directory) {
 /* Gets a gopher directory as a Uint8Array and returns a GopherDirectory. */
 var dirarray = decoder.decode(directory).split("\n");
 dirarray.forEach(function(entry, i, array) {
   array.splice(i, 1, rawToGopherEntry(entry));
 });
 return dirarray;
};

function GopherEntryToURL(entry) {
 /* Gets a GopherEntry and returns its URL. */
 let port;
 if (entry.port === 70 || !entry.port) {
   port="";
 } else {
   port=":" + String(entry.port);
 };
 let path = entry.path;
 if (entry.query) {
   path += "\t" + entry.query;
 };
 return encodeURI("gopher://" + entry.host + port + "/" + entry.type + path);
};

function URLToGopherEntry(url) {
 /* Gets a gopher:// URL and returns its GopherEntry, filling the name with host+path. */
 if (!url.startsWith("gopher://")) { return null; };
 url = decodeURI(url.slice(9));
 let urlarray = url.split(/\/(.*)/);
 let server = urlarray[0].split(":");
 let selector;
 if (urlarray[1]) {
   selector = urlarray[1].split("\t");
 } else {
   selector = ["", ""];
 };
 return {
   "type": selector[0][0] || "1",
   "name": server[0] + "/" + selector[0].slice(1),
   "path": selector[0].slice(1),
   "host": server[0],
   "port": parseInt(server[1],10) || 70,
   "query": selector[1] || ""
 };
};