/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80*/
/*global window, RSVP, rJS, WebTorrent, JSZip, tv4*/
(function (window, RSVP, rJS, document, crypto, WebTorrent, JSZip, tv4) {
  "use strict";

  var gadget_klass = rJS(window),
      template_element = gadget_klass.__template_element,
      torrent_info_template = Handlebars.compile(template_element
                                  .getElementById("torrent-info-template")
                                  .innerHTML);

  // Human readable bytes util
  function prettyBytes(num) {
    var exponent,
      unit,
      neg = num < 0,
      units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

    if (neg) {
      num = -num;
    }
    if (num < 1) {
      return (neg ? '-' : '') + num + ' B';
    }
    exponent = Math.min(Math.floor(Math.log(num) / Math.log(1000)),
                        units.length - 1);
    num = Number((num / Math.pow(1000, exponent)).toFixed(2));
    unit = units[exponent];
    return (neg ? '-' : '') + num + ' ' + unit;
  }

  function onProgress(gadget, torrent, element) {
    var num_peers = element.querySelector(".numPeers"),
        progressBar = element.querySelector(".progressBar"),
        downloaded = element.querySelector(".downloaded"),
        total = element.querySelector(".total"),
        remaining_html = element.querySelector(".remaining"),
        remaining,
        downloadSpeed = element.querySelector(".downloadSpeed"),
        uploadSpeed = element.querySelector(".uploadSpeed"),
        percent;
    // Peers
    num_peers.innerHTML = torrent.numPeers +
      (torrent.numPeers === 1 ? ' peer' : ' peers');

    total.innerHTML = prettyBytes(torrent.length);
    if (!downloaded) {
      uploadSpeed.innerHTML = prettyBytes(torrent.uploadSpeed) + '/s';
      progressBar.style.width = '100%';
      return;
    }

    // Download progress
    percent = Math.round(torrent.progress * 100 * 100) / 100;
    progressBar.style.width = percent + '%';
    downloaded.innerHTML = prettyBytes(torrent.downloaded);

    // Remaining time
    if (torrent.done) {
      remaining = 'Done.';
    } else {
      remaining = moment.duration(torrent.timeRemaining / 1000, 'seconds')
        .humanize();
      remaining = remaining[0].toUpperCase() +
        remaining.substring(1) + ' remaining.';
    }
    remaining_html.innerHTML = remaining;

    // Speed rates
    downloadSpeed.innerHTML = prettyBytes(torrent.downloadSpeed) + '/s';
  }

  function torrentHtmlElement(infohash, qclass) {
    var element = document.getElementById(infohash);
    if (qclass && element)
      return element.querySelector('.' + qclass);
    return element;
  }

  function createWebTorrent(rtcconfig) {
    return new WebTorrent({
      tracker: {
        rtcConfig: rtcconfig
      }
    });
  }

  gadget_klass

    .declareAcquiredMethod("jio_get", "jio_get")
    .declareAcquiredMethod("jio_put", "jio_put")
    .declareAcquiredMethod("jio_post", "jio_post")
    .declareAcquiredMethod("jio_allDocs", "jio_allDocs")
    .declareAcquiredMethod("notifySubmitted", "notifySubmitted")
    .declareAcquiredMethod("notifySubmitting", "notifySubmitting")
    .declareAcquiredMethod("getSetting", "getSetting")
    .declareAcquiredMethod("setSetting", "setSetting")
    .declareAcquiredMethod("redirect", "redirect")

    .ready(function () {
      var gadget = this;

      return gadget.getDeclaredGadget("model")
      .push(function (model) {
        return gadget.changeState({
          model_gadget: model,
          client: null,
          seed_index: {}
        });
      });
    })

    .declareMethod("render", function (options) {
      var gadget = this;

    })

    .declareMethod("digestMessage", function digestMessage(text, algorithm) {
      if (!algorithm)
        algorithm = 'SHA-256';
      return new RSVP.Queue()
        .push(function () {
          var uint8 = new TextEncoder().encode(text);
          return crypto.subtle.digest(algorithm, uint8);
        })
        .push(function (hash_buffer) {
          var hash_array;
          hash_array = Array.from(new Uint8Array(hash_buffer));
          return hash_array.map(b => b.toString(16).padStart(2, '0')).join('');
        });
    })

    .declareMethod("index_to_file", function (index_name) {
      var gadget = this,
          index_id = "index-" + index_name;
      return gadget.state.model_gadget.getAttachment(
        index_id,
        "zipped",
        {"format": "blob"})
      .push(function (result) {
        return new File([result], index_id + ".zip", {type: 'blob'});;
      })
      .push(undefined, function () {
        console.log("Building index blob");
        return gadget.notifySubmitted({
          message: "Building '" + index_name + "' torrent files...",
          status: "success"
        })
          .push(function () {
            return gadget.buildIndexBlob(index_name, index_id);
          });
      });
    })

    .declareMethod("buildIndexBlob", function (index_name, index_id) {
      var gadget = this,
          ids,
          file_list = [],
          zipped_file,
          zip = new JSZip(),
          name = index_id + ".zip";

      return gadget.state.model_gadget.getAttachment(
        index_id,
        "ids",
        {"format": "json"})
      .push(function (result) {
        ids = result;
        zip.file(index_id + '.ids', JSON.stringify(result));
        return gadget.state.model_gadget.getAttachment(
          index_id,
          "map",
          {"format": "text"});
      })
      .push(function (result) {
        zip.file(index_id + '.map', result);
        return gadget.state.model_gadget.getAttachment(
          index_id,
          "ctx",
          {"format": "text"});
      })
      .push(function (result) {
        var i,
            get_page,
            page_queue = new RSVP.Queue();

        get_page = function (page_id) {
          page_queue
            .push(function () {
              return gadget.state.model_gadget.get_page(page_id);
            })
            .push(function (page) {
              zip.file(page_id + '.json', JSON.stringify(page));
            });
        }

        zip.file(index_id + '.ctx', result);
        // get all page of this index
        for (i = 0; i < ids.length; i += 1) {
          get_page(ids[i].slice(1));
        }
        return page_queue
      })
      .push(function () {
        return new RSVP.Queue()
        .push(function () {
          return zip.generateAsync({
            type : "blob",
            compression: "DEFLATE",
            compressionOptions: {level: 9}
          })
            .then(function (zipped) {
              zipped_file = new File([zipped], name, {type: 'blob'});
              return gadget.state.model_gadget.putAttachment(
                index_id,
                "zipped",
                zipped
              );
            });
        })
        .push(function () {
          return zipped_file;
        });
      });
    })

    .declareMethod("file_to_index", function (torrent) {
      var gadget = this,
          extract_blob,
          blob_list = [],
          name_list = [],
          failed = false,
          file_list = torrent.files,
          msg_html;

      //quick check of file consitency here before insert
      extract_blob = function (err, blob) {
        var file;
        if (err || failed) {
          console.log("Failed to extract torrent file...");
          console.log(err);
          failed = true;
          return;
        }
        if (blob !== null && blob !== undefined) {
          blob_list.push(blob);
        }
        if (file_list.length > 0) {
          file = file_list.shift();
          name_list.push(file.name);
          // extract next blob file
          if (msg_html)
            msg_html.innerText = "Extracting file " + file.name + " ...";
          return file.getBlob(extract_blob);
        }
        else {
          // blob extracted, save them now
          if ((blob_list.length !== name_list.length) || failed) {
            console.log("Torrent extraction failed!!!");
            return;
          }
          return gadget.saveExtractedIndex(torrent,
                                           name_list,
                                           blob_list)
        }
      }
      // extract all blob from files
      msg_html = torrentHtmlElement(torrent.infoHash, 'message')
      return extract_blob();
    })

    .declareMethod("saveExtractedIndex", function (torrent,
                                                   ext_list,
                                                   blob_list) {
      var gadget = this,
          zip = new JSZip(),
          insert_data,
          zipped_blob,
          doc_blob,
          index_name,
          index_id,
          doc,
          ext,
          i,
          insert_blob,
          msg_html,
          schema;

      insert_blob = function (index_id, type, data) {
        return gadget.state.model_gadget.putAttachment(
          index_id,
          type,
          new Blob([data], {type : "application/json"}));
      };
      insert_data = function (name, data) {
        var type,
            id;
        if (name.endsWith('json')) {
          type = 'json';
          id = name.slice(0,-5);
        } else {
          type = name.slice(-3);
          id = name.slice(0,-4);
        }
        if (type === 'json') {
          return gadget.state.model_gadget.put(id, JSON.parse(data));
        } else if (type === 'ctx') {
          insert_blob(id, type, data);
        } else if (type === 'map') {
          insert_blob(id, type, data);
        } else if (type === 'ids') {
          index_name = id.split("index-")[1];
          index_id = id;
          insert_blob(id, type, data);
        }
      }

      msg_html = torrentHtmlElement(torrent.infoHash, 'message');
      // There is 2 files in the torrent, index doc and zipped data
      for (i = 0; i < ext_list.length; i += 1) {
        ext = ext_list[i].slice(-3);
        if (ext === "doc") {
          doc_blob = blob_list[i];
        }
        else if (ext === "zip")
          zipped_blob = blob_list[i];
      }
      return gadget.state.model_gadget.getSourceSchema()
      .push(function (s) {
        schema = s;
        return jIO.util.readBlobAsText(doc_blob);
      })
      .push(function (evt) {
        try {
          doc = JSON.parse(evt.target.result);
          if (!tv4.validate([doc], schema))
            return false;
        } catch (e) {
          console.error(e);
          return false;
        }
        if (msg_html)
          msg_html.innerText = "Uncompressing index data...";
        // extract zipped data here
        return zip.loadAsync(zipped_blob)
          .then(function (data) {
            var promise_list = [],
                get_zip;
            get_zip = function (name) {
              return zip.file(name).async('string')
                .then(function(data) {
                  if (msg_html)
                    msg_html.innerText = "Extracting file " + name + "...";
                  return insert_data(name, data);
                });
            }
            Object.keys(data.files).forEach(function (filename) {
                promise_list.push(get_zip(filename));
            });
            return RSVP.all(promise_list);
          });
      })
      .push(function (state) {
        if (state === false) {
          return gadget.notifySubmitted({
            message: "Error, the torrent contains invalid data!",
            status: "error"
          })
          .push(function () {
            return gadget.redirect({command: 'display', options: {
              page: "mynij_search"
            }});
          });
        }
        if (msg_html)
          msg_html.innerText = "Creating index document...";
        return gadget.state.model_gadget.createIndexDoc({
          title: doc.title,
          links: doc.links,
          description: doc.description,
          logo: doc.logo,
          magnet_uri: torrent.magnetURI,
          status: "built"
        })
        .push(function () {
            return gadget.state.model_gadget.pushIndex(index_name);
        })
        .push(function () {
          gadget.notifySubmitted({
            message: "Index '" + index_name + "' Saved.",
            status: "success"
          })
        })
        .push(function () {
          var zipped_file = new File([zipped_blob], name, {type: 'blob'});
          // Save this blob for future seed
          // this is done to try to keep same torrent magnet uri
          return gadget.state.model_gadget.putAttachment(
            index_id,
            "zipped",
            zipped_blob
          );
        })
        .push(function () {
          // stop seeding the torrent
          gadget.state.client.destroy(function (err) {
            if (err) {
              console.error(err.stack || err.message || err);
            }
          });
          if (gadget.state.redirect) {
            return gadget.redirect({command: 'display', options: {
              page: gadget.state.redirect
            }});
          }
          gadget.state.redirect = "";
        });
      });
    })

    .onLoop(function () {
      var gadget = this,
          container = document.getElementById('seedactivities'),
          seed_element,
          info,
          element,
          i,
          key_list = [];
      if (!container)
        return;
      info = container.querySelector('h2.info')
      seed_element = container.querySelector('.torrent-seed');
      key_list = Object.keys(gadget.state.seed_index);
      if (!key_list.length) {
        if (!info && seed_element) {
          info = document.createElement('h2');
          info.className = 'info';
          seed_element.appendChild(info);
        }
        info.innerText = "There is no torrent peer seeding!";
      }
      for (i = 0; i < key_list.length; i += 1) {
        let torrent = gadget.state.seed_index[key_list[i]];
        if (!torrent.ready) {
          continue;
        }
        if (torrentHtmlElement(torrent.infoHash)) {
          // already present
          continue;
        }
        if (info) {
          info.remove();
        }
        let torrent_template = torrent_info_template({
          magnet_title: torrent.comment,
          magnet_link: torrent.name,
          magnet_uri: torrent.magnetURI,
          torrent_link: torrent.torrentFileBlobURL,
          infohash: torrent.infoHash,
          total: prettyBytes(torrent.length),
          is_seed: true
        });
        container.innerHTML += torrent_template;
      }
    }, 5000)

    /*******************************/
    /**    Seed torrent here     **/
    /*****************************/

    .declareMethod("seedIndex", function (id) {
      var gadget = this,
          get_name,
          on_torrent,
          seed_file,
          seed_index,
          torrent_dict = [],
          torrent_name,
          index_doc;

      get_name = function (link_list) {
        var name = "";
        link_list.sort();
        name = link_list.join('');
        return gadget.digestMessage(name)
          .push(function (hash) {
             return hash + ".json";
          });
      }

      on_torrent = function (torrent) {
        gadget.state.seed_index[id] = torrent;
        torrent.on('warning', function (err) {
          console.error(err.stack || err.message || err)
        });

        torrent.on('error', function (err) {
          console.error(err.stack || err.message || err)
          return gadget.notifySubmitted({
            message: "Error: " + err.message || err,
            status: "error"
          });
        });

        torrent.on('upload', function (bytes) {
          var content = torrentHtmlElement(torrent.infoHash);
          if (content) {
            onProgress(gadget, torrent, content);
          }
        });

        console.log(torrent);
        return gadget.notifySubmitted({
            message: "Seeding index '" + torrent.comment + "'...",
            status: "success"
          })
          .push(function () {
            return gadget.state.model_gadget.updateIndexDoc(
              id,
              {magnet_uri: torrent.magnetURI});
          });
      };

      return gadget.jio_get(id)
        .push(function (doc) {
          var url_list = doc.links.replace('\n', ' ').match(/[^ ]+/g);
          index_doc = doc;
          return get_name(url_list)
        })
        .push(function (name) {
          torrent_name = name;
          torrent_dict[id] = name;
          return gadget.index_to_file(index_doc.title);
        })
        .push(function (index_blob) {
          var doc = {title: index_doc.title,
                     links: index_doc.links,
                     description: index_doc.description,
                     logo: index_doc.logo},
              index_file_list = [index_blob];
          // Add index document to blob
          index_file_list.push(new File(
            [new Blob([JSON.stringify(doc)],
                      {type : "application/json"})],
            index_doc.title + '.doc',
            {type: 'application/json'})
          );
          return gadget.state.client.seed(index_file_list, {
            name: torrent_name,
            createdBy: "Nexedi SA",
            private: false,
            announceList: [
              ['wss://tracker.openwebtorrent.com'],
              ['wss://softinst140244.host.vifib.net']
            ],
            comment: index_doc.title
           },
          on_torrent
        );
      })
      .push(undefined, function (err) {
        console.error(err);
        if (gadget.state.seed_index.hasOwnProperty(id)) {
          delete gadget.state.seed_index[id];
        }
        return gadget.notifySubmitted({
          message: "Failed to create torrent blob\n" +
            "Make sure your source is built!",
          status: "error"
        });
      });
    })

    .declareJob("seedAll", function () {
      var gadget = this;

      function notify(msg, status) {
        return gadget.notifySubmitted({
          message: msg,
          status: status || "error"
        });
      }

      function seedTorrent() {
        return gadget.jio_allDocs({
          query: 'portal_type: "Mynij Search Collection"',
          "select_list": ["title", "seed", "status"]
        })
          .push(function (result_list) {
            var i,
              promise_list = [],
              torrent_id;
            console.log(result_list);
            for (i = 0; i < result_list.data.rows.length; i += 1) {
              let item = result_list.data.rows[i];
              if (item.value.seed === "on" && item.value.status !== "built") {
                promise_list.push(
                  notify("Cannot seed '" + item.value.title +
                         "', the source is not built!"));
                continue;
              }
              if (item.value.seed === "on" &&
                  !gadget.state.seed_index.hasOwnProperty(item.id)) {
                gadget.state.seed_index[item.id] = {};
                promise_list.push(gadget.seedIndex(item.id));
                console.log("call seed to " + item.id);
              } else if (!item.value.seed || item.value.seed === "off") {
                if (gadget.state.seed_index.hasOwnProperty(item.id)) {
                  // remove torrent seed
                  torrent_id = gadget.state.seed_index[item.id].infoHash
                  gadget.state.seed_index[item.id].destroy(function () {
                    var element = torrentHtmlElement(torrent_id);
                    console.log("Torrent of '" + item.value.title +
                                "' destroyed...");
                    if (element)
                      element.remove();
                  });
                  delete gadget.state.seed_index[item.id];
                }
              }
            }
            return RSVP.all(promise_list);
        })
        .push(function () {
          return RSVP.delay(60000);
        })
        .push(function () {
          return seedTorrent();
        });
      }

      return gadget.state.model_gadget.getRtcconfig()
        .push(function (config) {
          gadget.state.client = createWebTorrent(config);
          return seedTorrent();
        });
    })

    .declareJob("download", function (torrent_id, redirect) {
      var gadget = this,
          torrent_template,
          container = gadget.element.querySelector('.torrent-download');

      if (!torrent_id) {
        return;
      }
      torrent_id = torrent_id.trim();
      gadget.state.redirect = redirect;
      container.classList.remove('hidden');
      return gadget.state.model_gadget.getRtcconfig()
      .push(function (config) {
        gadget.state.client = createWebTorrent(config);
        return gadget.notifySubmitting();
      })
      .push(function () {
        // Download the torrent
        return gadget.state.client.add(torrent_id, function (torrent) {
          var container_h2 = container.querySelector("h2.info"),
              element;

          function onDone () {
            clearInterval(gadget.state.interval);
            onProgress(gadget, torrent, element);
            return gadget.file_to_index(torrent);
          }

          if (container_h2) {
            container_h2.remove();
          }
          torrent_template = torrent_info_template({
            magnet_title: torrent.comment || torrent.files[0].name.slice(0, -4),
            magnet_link: torrent.name,
            torrent_link: torrent.torrentFileBlobURL,
            infohash: torrent.infoHash,
            total: prettyBytes(torrent.length),
            is_seed: false
          });
          container.innerHTML = torrent_template;

          element = gadget.element.querySelector(".torrentBoddy");
          torrent.on('error', function (err) {
            clearInterval(gadget.state.interval);
            console.error(err.stack || err.message || err);
            return gadget.notifySubmitted({
              message: "Error: " + err.message || err,
              status: "error"
            });
          });

          // Trigger statistics refresh
          torrent.on('done', onDone);
          gadget.state.interval = setInterval(function(){
            onProgress(gadget, torrent, element);
          }, 500);

          onProgress(gadget, torrent, element);
        });
      })
      .push(function (torrent) {
        torrent.on('error', function (err) {
          clearInterval(gadget.state.interval);
          console.error(err.stack || err.message || err);
          return gadget.notifySubmitted({
            message: "Error: " + err.message || err,
            status: "error"
          });
        });
      });
    });

}(window, RSVP, rJS, document, crypto, WebTorrent, JSZip, tv4));