"use strict";
// License: MIT

import { VirtualTable } from "../uikit/lib/table";
import ModalDialog from "../uikit/lib/modal";
import { ContextMenu } from "./contextmenu";
import { iconForPath } from "../lib/windowutils";
import { _, localize } from "../lib/i18n";
import { Prefs } from "../lib/prefs";
import { MASK, FASTFILTER, SUBFOLDER } from "../lib/recentlist";
import { WindowState } from "./windowstate";
import { Dropdown } from "./dropdown";
import { Keys } from "./keys";
import { Icons } from "./icons";
import { sort, naturalCaseCompare } from "../lib/sorting";
import { hookButton } from "../lib/manager/renamer";
import { CellTypes } from "../uikit/lib/constants";
// eslint-disable-next-line no-unused-vars
import { runtime, RawPort } from "../lib/browser";
import { $ } from "./winutil";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "../lib/item";
// eslint-disable-next-line no-unused-vars
import { ItemDelta } from "../lib/select";
// eslint-disable-next-line no-unused-vars
import { TableConfig } from "../uikit/lib/config";
import { validateSubFolder as validateSubfolder } from "../lib/util";
import "./theme";

const PORT: RawPort = runtime.connect(null, { name: "select" });

const TREE_CONFIG_VERSION = 1;

const COL_CHECK = 0;
const COL_DOWNLOAD = 1;
const COL_TITLE = 2;
const COL_DESC = 3;
const COL_MASK = 4;
const COL_REFERRER = 5;

const ICON_BASE_SIZE = 16;
const NUM_FILTER_CLASSES = 8;

let Table: SelectionTable;
let Mask: Dropdown;
let FastFilter: Dropdown;
let Subfolder: Dropdown;


type DELTAS = {deltaLinks: ItemDelta[]; deltaMedia: ItemDelta[]};

interface BaseMatchedItem extends BaseItem {
  matched?: string | null;
  rowid: number;
}

function clearErrors() {
  const not = $("#notification");
  not.textContent = "";
  not.style.display = "none";
}


function matched(item: BaseMatchedItem) {
  return item && item.matched && item.matched !== "unmanual";
}

class PausedModalDialog extends ModalDialog {
  getContent() {
    const tmpl = $<HTMLTemplateElement>("#paused-template");
    const content = tmpl.content.cloneNode(true) as DocumentFragment;
    return content;
  }

  shown() {
    this.focusDefault();
  }

  get buttons() {
    return [
      {
        title: _("remember"),
        value: "ok",
        default: true,
        dismiss: false
      },
      {
        title: _("add-paused-once"),
        value: "once",
        default: false,
        dismiss: false
      },
      {
        title: _("cancel"),
        value: "cancel",
        default: false,
        dismiss: true
      }
    ];
  }
}

class CheckClasser extends Map<string, string> {
  gen: IterableIterator<string>;

  constructor(numClasses: number) {
    super();
    this.gen = (function *() {
      for (;;) {
        for (let c = 0; c < numClasses; ++c) {
          yield `filter-${c + 1}`;
        }
      }
    })();
    this.set("manual", "filter-manual");
    this.set("fast", "filter-fast");
  }

  "get"(key: string) {
    let result = super.get(key);
    if (typeof result !== "string") {
      result = this.gen.next().value;
      if (result) {
        super.set(key, result);
      }
    }
    return result;
  }
}

type KeyFn = (item: BaseMatchedItem) => any;

class ItemCollection {
  private items: BaseMatchedItem[];

  private indexes: Map<number, BaseMatchedItem>;

  constructor(items: BaseMatchedItem[]) {
    this.items = items;
    this.assignRows();
    this.indexes = new Map(items.map(i => [i.idx, i]));
  }

  assignRows() {
    this.items.forEach((item, idx) => item.rowid = idx);
  }

  get length() {
    return this.items.length;
  }

  get checked() {
    const rv: number[] = [];
    this.items.forEach(function (item, idx) {
      if (item.matched && item.matched !== "unmanual") {
        rv.push(idx);
      }
    });
    return rv;
  }

  get checkedIndexes() {
    const rv: number[] = [];
    this.items.forEach(function (item) {
      if (item.matched && item.matched !== "unmanual") {
        rv.push(item.idx);
      }
    });
    return rv;
  }


  at(idx: number) {
    return this.items[idx];
  }

  byIndex(idx: number) {
    return this.indexes.get(idx);
  }

  sort(keyFn: KeyFn) {
    sort(this.items, keyFn, naturalCaseCompare);
    this.assignRows();
  }

  reverse() {
    this.items.reverse();
    this.assignRows();
  }

  filter(fn: (item: BaseMatchedItem, idx: number) => boolean) {
    return this.items.filter(fn);
  }
}

class SelectionTable extends VirtualTable {
  checkClasser: CheckClasser;

  icons: Icons;

  links: ItemCollection;

  media: ItemCollection;

  items: ItemCollection;

  type: string;

  status: HTMLElement;

  linksTab: HTMLElement;

  mediaTab: HTMLElement;

  linksFilters: HTMLElement;

  mediaFilters: HTMLElement;

  contextMenu: ContextMenu;

  sortcol: number | null;

  sortasc: boolean;

  keyfns: Map<string, KeyFn>;

  constructor(
      treeConfig: TableConfig | null, type: string,
      links: BaseMatchedItem[], media: BaseMatchedItem[]) {
    if (type === "links" && !links.length) {
      type = "media";
    }
    else if (type === "media" && !media.length) {
      type = "links";
    }
    super("#items", treeConfig, TREE_CONFIG_VERSION);

    this.checkClasser = new CheckClasser(NUM_FILTER_CLASSES);
    this.icons = new Icons($("#icons") as HTMLStyleElement);
    this.links = new ItemCollection(links);
    this.media = new ItemCollection(media);
    this.type = type;
    this.items = type === "links" ? this.links : this.media;

    this.status = $("#statusItems");
    this.linksTab = $("#linksTab");
    if (!links.length) {
      this.linksTab.classList.add("disabled");
    }
    else {
      this.linksTab.addEventListener(
        "click", this.switchTab.bind(this, "links"));
    }

    this.mediaTab = $("#mediaTab");
    if (!media.length) {
      this.mediaTab.classList.add("disabled");
    }
    else {
      this.mediaTab.addEventListener(
        "click", this.switchTab.bind(this, "media"));
    }
    this.linksFilters = $("#linksFilters");
    this.mediaFilters = $("#mediaFilters");

    localize(($("#table-context") as HTMLTemplateElement).content);
    this.contextMenu = new ContextMenu("#table-context");
    Keys.adoptContext(this.contextMenu);

    this.sortcol = null;
    this.sortasc = true;
    this.keyfns = new Map<string, KeyFn>([
      ["colDownload", item => item.usable],
      ["colTitle", item => [item.title, item.usable]],
      ["colDescription", item => [item.description, item.usable]],
      ["colMask", item => [item.mask, item.usable]],
    ]);

    this.on("config-changed", () => {
      Prefs.set("tree-config-select", JSON.stringify(this));
    });
    this.on("column-clicked", colid => {
      const keyfn = this.keyfns.get(colid);
      if (!keyfn) {
        return false;
      }
      this.links.sort(keyfn);
      this.media.sort(keyfn);
      const elem = document.querySelector<HTMLElement>(`#${colid}`);
      const oldelem = (this.sortcol && document.querySelector<HTMLElement>(`#${this.sortcol}`));
      if (this.sortcol === colid && this.sortasc) {
        this.links.reverse();
        this.media.reverse();
        this.sortasc = false;
      }
      else {
        this.sortcol = colid;
        this.sortasc = true;
      }
      if (oldelem) {
        oldelem.dataset.sortdir = "";
      }
      if (elem) {
        elem.dataset.sortdir = this.sortasc ? "asc" : "desc";
      }
      this.invalidate();
      return true;
    });
    this.on(" -keypress", () => this.checkSelection());
    this.contextMenu.on("ctx-check-selected", () => {
      this.checkSelection("manual");
    });
    this.contextMenu.on("ctx-uncheck-selected", () => {
      this.checkSelection("unmanual");
    });
    this.contextMenu.on("ctx-toggle-selected", () => {
      this.checkSelection("toggle");
    });

    Keys.on("ACCEL-KeyA", (event: KeyboardEvent) => {
      const target = event.target as HTMLElement;
      if (target.localName === "input") {
        return false;
      }
      this.selectAll();
      return true;
    });

    Keys.on("ACCEL-KeyF", () => {
      this.selectChecked();
      return true;
    });

    Keys.on("ACCEL-KeyI", () => {
      this.selectToggle();
      return true;
    });

    Keys.on("ACCEL-KeyO", () => {
      this.openSelection();
      return true;
    });

    this.contextMenu.on("ctx-mask", async() => {
      if (this.selection.empty) {
        return;
      }
      let oldmask = "";
      for (const r of this.selection) {
        const m = this.items.at(r).mask;
        if (oldmask && m !== oldmask) {
          oldmask = "";
          break;
        }
        oldmask = m || oldmask;
      }
      try {
        Keys.suppressed = true;
        const newmask = await ModalDialog.prompt(
          _("set_mask"), _("set_mask_text"), oldmask);
        for (const r of this.selection) {
          this.items.at(r).mask = newmask;
          this.invalidateRow(r);
        }
      }
      catch (ex) {
        console.warn("mask dismissed", ex);
      }
      finally {
        Keys.suppressed = false;
      }
    });

    this.contextMenu.on("ctx-referrer", async() => {
      if (this.selection.empty) {
        return;
      }
      let oldref = "";
      for (const r of this.selection) {
        const m = this.items.at(r).usableReferrer;
        if (oldref && m !== oldref) {
          oldref = "";
          break;
        }
        oldref = m || oldref;
      }
      try {
        Keys.suppressed = true;
        const newref = await ModalDialog.prompt(
          _("set_referrer"), _("set_referrer_text"), oldref);
        try {
          let ref;
          if (!newref) {
            ref = {
              referrer: undefined,
              usableReferrer: undefined,
            };
          }
          else {
            const u = new URL(newref);
            u.hash = "";
            ref = {
              referrer: u.toString(),
              usableReferrer: decodeURIComponent(u.toString()),
            };
          }
          for (const r of this.selection) {
            Object.assign(this.items.at(r), ref);
            this.invalidateRow(r);
          }
        }
        catch {
          // ignored
        }
      }
      catch (ex) {
        console.warn("mask dismissed", ex);
      }
      finally {
        Keys.suppressed = false;
      }
    });


    this.contextMenu.on("dismissed", () => this.table.focus());

    this.on("contextmenu", (tree, event) => {
      if (!this.selection.empty) {
        this.contextMenu.show(event);
      }
      return true;
    });

    this.init();
    this.switchTab(type);
  }

  get rowCount() {
    return this.items.length;
  }

  checkSelection(state?: string) {
    if (this.selection.empty) {
      return false;
    }
    for (const rowid of this.selection) {
      const item = this.items.at(rowid);
      if (!state) {
        state = matched(item) ? "unmanual" : "manual";
      }
      let ns;
      switch (state) {
      case "toggle":
        ns = matched(item) ? "unmanual" : "manual";
        break;
      default:
        ns = state;
      }
      item.matched = ns;
      this.invalidateRow(rowid);
    }
    this.updateStatus();
    return true;
  }

  selectAll() {
    this.selection.add(0, this.rowCount - 1);
  }

  selectChecked() {
    this.selection.clear();
    let min = null;
    for (const ci of this.items.checked) {
      this.selection.add(ci);
      min = min === null ? ci : Math.min(min, ci);
    }
    if (min !== null) {
      this.scrollIntoView(min);
    }
  }

  selectToggle() {
    this.selection.toggle(0, this.rowCount - 1);
  }

  openSelection() {
    const privates: BaseMatchedItem[] = [];
    const items = this.items.filter((i, idx) => this.selection.contains(idx)).
      filter(i => {
        if (i.private) {
          privates.push(i);
          return false;
        }
        return true;
      });
    if (!items.length && !privates.length) {
      if (this.focusRow < 0) {
        return;
      }
      const item = this.items.at(this.focusRow);
      if (item.private) {
        privates.push(item);
      }
      else {
        items.push(item);
      }
    }

    if (items.length) {
      PORT.postMessage({
        msg: "openUrls",
        urls: items.map(e => e.url),
        incognito: false,
      });
    }
    if (privates.length) {
      PORT.postMessage({
        msg: "openUrls",
        urls: privates.map(e => e.url),
        incognito: true,
      });
    }
  }

  applyDeltaTo(delta: ItemDelta[], items: ItemCollection) {
    const active = items === this.items;
    for (const d of delta) {
      const {idx = -1, matched = null} = d;
      if (idx < 0) {
        continue;
      }
      const item = items.byIndex(idx);
      if (!item) {
        continue;
      }
      if (item.matched === matched) {
        continue;
      }
      if (matched !== "fast" &&
        (item.matched === "manual" || item.matched === "unmanual")) {
        // Skip manually selected items
        continue;
      }
      item.matched = matched;
      if (active) {
        this.invalidateRow(item.rowid);
      }
    }
  }

  applyDeltas({deltaLinks = [], deltaMedia = []}: DELTAS) {
    this.applyDeltaTo(deltaLinks, this.links);
    this.applyDeltaTo(deltaMedia, this.media);
    this.updateStatus();
  }

  switchTab(type: string) {
    this.type = type;
    const isLinks = type === "links";
    this.linksTab.classList[isLinks ? "add" : "remove"]("active");
    this.mediaTab.classList[!isLinks ? "add" : "remove"]("active");
    this.linksFilters.classList[isLinks ? "add" : "remove"]("active");
    this.mediaFilters.classList[!isLinks ? "add" : "remove"]("active");
    this.items = (this as any)[type];
    this.selection.clear();
    this.invalidate();
    this.updateStatus();
  }

  updateStatus() {
    const selected = this.items.checked.length;
    if (!selected) {
      this.status.textContent = _("noitems.label");
    }
    else {
      this.status.textContent = _("numitems.label", [selected]);
    }
    clearErrors();
  }

  getRowClasses(rowid: number) {
    const item = this.items.at(rowid);
    if (!item || !matched(item) || !item.matched) {
      return null;
    }
    const m = this.checkClasser.get(item.matched);
    if (!m) {
      return null;
    }
    return ["filtered", m];
  }

  getCellIcon(rowid: number, colid: number) {
    const item = this.items.at(rowid);
    if (item && colid === COL_DOWNLOAD) {
      return this.icons.get(iconForPath(item.url, ICON_BASE_SIZE));
    }
    return null;
  }

  getCellType(rowid: number, colid: number) {
    switch (colid) {
    case COL_CHECK:
      return CellTypes.TYPE_CHECK;
    default:
      return CellTypes.TYPE_TEXT;
    }
  }

  getDownloadText(idx: number) {
    const item = this.items.at(idx);
    if (!item) {
      return "";
    }
    if (item.fileName) {
      return `${item.usable} (${item.fileName})`;
    }
    return item.usable;
  }

  getText(prop: string, idx: number) {
    const item: any = this.items.at(idx);
    if (!item || !(prop in item) || !item[prop]) {
      return "";
    }
    return item[prop];
  }

  getMaskText(idx: number) {
    const item = this.items.at(idx);
    if (item) {
      return item.mask;
    }
    return _("mask.default");
  }

  getCellText(rowid: number, colid: number) {
    switch (colid) {
    case COL_DOWNLOAD:
      return this.getDownloadText(rowid);

    case COL_TITLE:
      return this.getText("title", rowid);

    case COL_DESC:
      return this.getText("description", rowid);

    case COL_REFERRER:
      return this.getText("usableReferrer", rowid);

    case COL_MASK:
      return this.getMaskText(rowid);

    default:
      return "";
    }
  }

  getCellCheck(rowid: number, colid: number) {
    if (colid === COL_CHECK) {
      return !!matched(this.items.at(rowid));
    }
    return false;
  }

  setCellCheck(rowid: number, colid: number, value: boolean) {
    this.items.at(rowid).matched = value ? "manual" : "unmanual";
    this.invalidateRow(rowid);
    this.updateStatus();
  }
}

async function download(paused = false) {
  try {
    const mask = Mask.value;
    if (!mask) {
      throw new Error("error.invalidMask");
    }
    const subfolder = Subfolder.value;
    validateSubfolder(subfolder);

    const items = Table.items.checkedIndexes;
    if (!items.length) {
      throw new Error("error.noItemsSelected");
    }
    if (paused && !(await Prefs.get("add-paused"))) {
      try {
        Keys.suppressed = true;
        const remember = await new PausedModalDialog().show();
        if (remember === "ok") {
          await Prefs.set("add-paused", true);
          await Prefs.save();
        }
      }
      catch (ex) {
        return;
      }
      finally {
        Keys.suppressed = false;
      }
    }
    if (!paused) {
      await Prefs.set("add-paused", false);
    }
    PORT.postMessage({
      msg: "queue",
      items,
      options: {
        type: Table.type,
        paused,
        mask,
        maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
        fast: FastFilter.value,
        fastOnce: $<HTMLInputElement>("#fastOnceCheck").checked,
        subfolder,
        subfolderOnce: $<HTMLInputElement>("#subfolderOnceCheck").checked,
      }
    });
  }
  catch (ex) {
    const not = $("#notification");
    const msg = _(ex.message || ex);
    not.textContent = msg || ex.message || ex;
    not.style.display = "block";
  }
}

class Filter {
  active: boolean;

  checkElem: HTMLInputElement;

  container: HTMLElement;

  elem: HTMLLabelElement;

  id: string;

  label: string;

  constructor(container: HTMLElement, raw: any, active = false) {
    Object.assign(this, raw);
    this.active = active;
    this.container = container;

    this.elem = document.createElement("label");
    this.elem.classList.add("filter");
    this.elem.setAttribute("title", this.elem.textContent = this.label);
    this.checkElem = document.createElement("input");
    this.checkElem.setAttribute("type", "checkbox");
    this.checkElem.checked = active;
    this.elem.insertBefore(this.checkElem, this.elem.firstChild);
    this.container.appendChild(this.elem);
    this.checkElem.addEventListener("change", this.changed.bind(this));
  }

  changed() {
    PORT.postMessage({
      msg: "filter-changed",
      id: this.id,
      value: this.checkElem.checked
    });
  }
}

function setFiltersInternal(
    desc: string, filters: any[], active: Set<string>) {
  const container = $(desc);
  container.textContent = "";
  for (let filter of filters) {
    filter = new Filter(container, filter, active.has(filter.id));
  }
}

function setFilters(filters: any) {
  const {
    linkFilterDescs = [],
    mediaFilterDescs = [],
    activeFilters = []
  } = filters;
  const active: Set<string> = new Set(activeFilters);
  setFiltersInternal("#linksFilters", linkFilterDescs, active);
  setFiltersInternal("#mediaFilters", mediaFilterDescs, active);
}

function cancel() {
  PORT.postMessage("cancel");
  return true;
}

async function init() {
  await Promise.all([MASK.init(), FASTFILTER.init(), SUBFOLDER.init()]);
  Mask = new Dropdown("#mask", MASK.values);
  Mask.on("changed", clearErrors);
  FastFilter = new Dropdown("#fast", FASTFILTER.values);
  FastFilter.on("changed", () => {
    PORT.postMessage({
      msg: "fast-filter",
      fastFilter: FastFilter.value
    });
  });
  Subfolder = new Dropdown("#subfolder", SUBFOLDER.values);
  Subfolder.on("changed", clearErrors);
}

const LOADED = new Promise(resolve => {
  addEventListener("load", function dom() {
    removeEventListener("load", dom);
    resolve();
  });
});


addEventListener("DOMContentLoaded", function dom() {
  removeEventListener("DOMContentLoaded", dom);

  init().catch(console.error);

  localize(document.documentElement);

  $("#donate").addEventListener("click", () => {
    PORT.postMessage({
      msg: "donate",
    });
  });
  $("#statusPrefs").addEventListener("click", () => {
    PORT.postMessage({
      msg: "prefs",
    });
  });
  $("#btnDownload").addEventListener("click", () => download(false));
  $("#btnPaused").addEventListener("click", () => download(true));
  $("#btnCancel").addEventListener(
    "click", cancel);
  $("#fastDisableOthers").addEventListener("change", () => {
    PORT.postMessage({
      msg: "onlyfast",
      fast: $<HTMLInputElement>("#fastDisableOthers").checked
    });
  });

  Keys.on("Enter", "Return", () => {
    download(false);
    return true;
  });
  Keys.on("ACCEL-Enter", "ACCEL-Return", () => {
    download(true);
    return true;
  });

  PORT.onMessage.addListener(async (msg: any) => {
    try {
      await LOADED;
      switch (msg.msg) {
      case "items": {
        const {type = "links", links = [], media = []} = msg.data;
        const treeConfig = JSON.parse(
          await Prefs.get("tree-config-select", "{}"));
        requestAnimationFrame(() => {
          Table = new SelectionTable(treeConfig, type, links, media);
        });
        return;
      }

      case "filters":
        setFilters(msg.data);
        return;

      case "item-delta":
        requestAnimationFrame(() => {
          Table.applyDeltas(msg.data);
        });
        return;

      default:
        throw Error("Unhandled message");
      }
    }
    catch (ex) {
      console.error("Failed to process message", msg, ex);
    }
  });

  Keys.on("Escape", cancel);

  hookButton($("#maskButton"));
});

addEventListener("contextmenu", event => {
  event.preventDefault();
  event.stopPropagation();
  return false;
});


addEventListener("beforeunload", function() {
  PORT.disconnect();
});

new WindowState(PORT);
