/* global $$ $ $create $createLink $$remove showSpinner */// dom.js
/* global API */// msg.js
/* global CODEMIRROR_THEMES */
/* global CodeMirror */
/* global URLS closeCurrentTab deepEqual */// toolbox.js
/* global compareVersion */// cmpver.js
/* global messageBox */
/* global prefs */
/* global preinit */
/* global styleCodeEmpty */// sections-util.js
/* global t */// localization.js
'use strict';

const CFG_SEL = '#message-box.config-dialog';
let cfgShown = true;

let cm;
let initialUrl;
let installed;
let installedDup;
let liveReload;
let tabId;
let vars;

// "History back" in Firefox (for now) restores the old DOM including the messagebox,
// which stays after installing since we don't want to wait for the fadeout animation before resolving.
document.on('visibilitychange', () => {
  $$remove('#message-box:not(.config-dialog)');
  if (installed) liveReload.onToggled();
});

setTimeout(() => !cm && showSpinner($('#header')), 200);

/*
 * Preinit starts to download as early as possible,
 * then the critical rendering path scripts are loaded in html,
 * then the meta of the downloaded code is parsed in the background worker,
 * then CodeMirror scripts/css are added so they can load while the worker runs in parallel,
 * then the meta response arrives from API and is immediately displayed in CodeMirror,
 * then the sections of code are parsed in the background worker and displayed.
 */
(async function init() {
  const theme = prefs.get('editor.theme');
  if (theme !== 'default') {
    document.head.append($create('style', CODEMIRROR_THEMES[theme] || ''));
  }
  ({tabId, initialUrl} = preinit);
  liveReload = initLiveReload();
  preinit.tpl.then(el => {
    $('#ss-scheme').append(...$('#ss-scheme', el).children);
    prefs.subscribe('schemeSwitcher.enabled', (_, val) => {
      $('#ss-scheme-off').hidden = val !== 'never';
    }, {runNow: true});
  });

  const [
    {dup, style, error, sourceCode},
    hasFileAccess,
  ] = await Promise.all([
    preinit.ready,
    API.data.get('hasFileAccess'),
  ]);
  if (!style && sourceCode == null) {
    messageBox.alert(isNaN(error) ? `${error}` : 'HTTP Error ' + error, 'pre');
    return;
  }
  cm = CodeMirror($('.main'), {
    value: sourceCode || style.sourceCode,
    readOnly: true,
    colorpicker: true,
    theme,
  });
  window.on('resize', adjustCodeHeight);
  if (error) {
    showBuildError(error);
  }
  if (!style) {
    return;
  }
  const data = style.usercssData;
  const dupData = dup && dup.usercssData;
  const versionTest = dup && compareVersion(data.version, dupData.version);

  updateMeta(style, dup);

  // update UI
  if (versionTest < 0) {
    $('h1').after($create('.warning', t('versionInvalidOlder')));
  }
  $('button.install').onclick = () => {
    shouldShowConfig();
    (!dup ?
      Promise.resolve(true) :
      messageBox.confirm($create('span', t('styleInstallOverwrite', [
        data.name + (dup.customName ? ` (${dup.customName})` : ''),
        dupData.version,
        data.version,
      ])))
    ).then(ok => ok &&
      API.usercss.install(style)
        .then(install)
        .catch(err => messageBox.alert(t('styleInstallFailed', err), 'pre'))
    );
  };

  // set updateUrl
  const checker = $('.set-update-url input[type=checkbox]');
  const updateUrl = new URL(style.updateUrl || initialUrl);
  if (dup && dup.updateUrl === updateUrl.href) {
    checker.checked = true;
    // there is no way to "unset" updateUrl, you can only overwrite it.
    checker.disabled = true;
  } else if (updateUrl.protocol !== 'file:' || hasFileAccess) {
    checker.checked = true;
    style.updateUrl = updateUrl.href;
  }
  checker.onchange = () => {
    style.updateUrl = checker.checked ? updateUrl.href : null;
  };
  checker.onchange();
  $('.set-update-url p').textContent = updateUrl.href.length < 300 ? updateUrl.href :
    updateUrl.href.slice(0, 300) + '...';

  // set prefer scheme
  $('#ss-scheme').onchange = e => {
    style.preferScheme = e.target.value;
  };

  if (URLS.isLocalhost(initialUrl)) {
    $('.live-reload input').onchange = liveReload.onToggled;
  } else {
    $('.live-reload').remove();
  }
})();

function updateMeta(style, dup = installedDup) {
  installedDup = dup;
  const data = style.usercssData;
  const dupData = dup && dup.usercssData;
  const versionTest = dup && compareVersion(data.version, dupData.version);

  cm.setPreprocessor(data.preprocessor);

  const installButtonLabel = t(
    installed ? 'installButtonInstalled' :
    !dup ? 'installButton' :
    versionTest > 0 ? 'installButtonUpdate' : 'installButtonReinstall'
  );
  document.title = `${installButtonLabel} ${data.name}`;

  $('.install').textContent = installButtonLabel;
  $('.install').classList.add(
    installed ? 'installed' :
    !dup ? 'install' :
    versionTest > 0 ? 'update' :
    'reinstall');
  $('.set-update-url').title = dup && dup.updateUrl &&
    (t('installUpdateFrom', dup.updateUrl) || '').replace(/\S+$/, '\n$&');
  $('.meta-name').textContent = data.name;
  $('.meta-version').textContent = data.version;
  $('.meta-description').textContent = data.description;
  $$('#ss-scheme input').forEach(el => {
    el.checked = el.value === (style.preferScheme || 'none');
  });

  replaceChildren($('.meta-author'), makeAuthor(data.author), true);
  replaceChildren($('.meta-license'), data.license, true);
  replaceChildren($('.external-link'), makeExternalLink());
  getAppliesTo(style).then(list =>
    replaceChildren($('.applies-to'), list.map(s => $create('li', s))));

  Object.assign($('.configure-usercss'), {
    hidden: !data.vars,
    onclick: openConfigDialog,
  });
  if (!data.vars) {
    cfgShown = false;
    $$remove(CFG_SEL);
  } else if (!deepEqual(data.vars, vars)) {
    vars = data.vars;
    // Use the user-customized vars from the installed style
    for (const [dk, dv] of Object.entries(dup && dupData.vars || {})) {
      const v = vars[dk];
      if (v && v.type === dv.type) {
        v.value = dv.value;
      }
    }
  }
  if (shouldShowConfig()) {
    openConfigDialog();
  }

  $('#header').dataset.arrivedFast = performance.now() < 500;
  $('#header').classList.add('meta-init');
  $('#header').classList.remove('meta-init-error');

  setTimeout(() => $$remove('.lds-spinner'), 1000);
  showError('');
  requestAnimationFrame(adjustCodeHeight);
  if (dup) enablePostActions();

  function makeAuthor(text) {
    const match = text && text.match(/^(.+?)(?:\s+<(.+?)>)?(?:\s+\((.+?)\))?$/);
    if (!match) {
      return text;
    }
    const [, name, email, url] = match;
    const elems = [];
    if (email) {
      elems.push($createLink(`mailto:${email}`, name));
    } else {
      elems.push($create('span', name));
    }
    if (url) {
      elems.push($createLink(url,
        $create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'},
          $create('SVG:path', {
            d: 'M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z',
          }))
      ));
    }
    return elems;
  }

  function makeExternalLink() {
    const urls = [
      data.homepageURL && [data.homepageURL, t('externalHomepage')],
      data.supportURL && [data.supportURL, t('externalSupport')],
    ];
    return (data.homepageURL || data.supportURL) && (
      $create('div', [
        $create('h3', t('externalLink')),
        $create('ul', urls.map(args => args &&
          $create('li',
            $createLink(...args)
          )
        )),
      ]));
  }

  async function openConfigDialog() {
    await require(['/js/dlg/config-dialog']); /* global configDialog */
    configDialog(style);
  }
}

function showError(err) {
  $('.warnings').textContent = '';
  $('.warnings').classList.toggle('visible', Boolean(err));
  document.body.classList.toggle('has-warnings', Boolean(err));
  err = Array.isArray(err) ? err : [err];
  if (err[0]) {
    let i;
    if ((i = err[0].index) >= 0 ||
        (i = err[0].offset) >= 0) {
      cm.jumpToPos(cm.posFromIndex(i));
      cm.setSelections(err.map(e => {
        const pos = e.index >= 0 && cm.posFromIndex(e.index) || // usercss meta parser
          e.offset >= 0 && {line: e.line - 1, ch: e.col - 1}; // csslint code parser
        return pos && {anchor: pos, head: pos};
      }).filter(Boolean));
      cm.focus();
    }
    $('.warnings').appendChild(
      $create('.warning', [
        t('parseUsercssError'),
        '\n',
        ...err.map(e => e.message ? $create('pre', e.message) : e || 'Unknown error'),
      ]));
  }
  adjustCodeHeight();
}

function showBuildError(error) {
  $('#header').classList.add('meta-init-error');
  console.error(error);
  showError(error);
}

function install(style) {
  installed = style;

  $$remove('.warning');
  $('button.install').disabled = true;
  $('button.install').classList.add('installed');
  $('#live-reload-install-hint').hidden = !liveReload.enabled;
  $('.set-update-url').title = style.updateUrl ?
    t('installUpdateFrom', style.updateUrl) : '';
  $$('.install-disable input').forEach(el => (el.disabled = true));
  document.body.classList.add('installed');
  enablePostActions();
  updateMeta(style);
}

function enablePostActions() {
  const {id} = installed || installedDup;
  sessionStorage.justEditedStyleId = id;
  $('#edit').search = `?id=${id}`;
  $('#delete').onclick = async () => {
    if (await messageBox.confirm(t('deleteStyleConfirm'), 'danger center', t('confirmDelete'))) {
      await API.styles.delete(id);
      if (tabId < 0 && history.length > 1) {
        history.back();
      } else {
        closeCurrentTab();
      }
    }
  };
}

async function getAppliesTo(style) {
  if (style.sectionsPromise) {
    try {
      style.sections = await style.sectionsPromise;
    } catch (error) {
      showBuildError(error);
      return [];
    } finally {
      delete style.sectionsPromise;
    }
  }
  let numGlobals = 0;
  const res = [];
  const TARGETS = ['urls', 'urlPrefixes', 'domains', 'regexps'];
  for (const section of style.sections) {
    const targets = [].concat(...TARGETS.map(t => section[t]).filter(Boolean));
    res.push(...targets);
    numGlobals += !targets.length && !styleCodeEmpty(section.code);
  }
  res.sort();
  if (!res.length || numGlobals) {
    res.push(t('appliesToEverything'));
  }
  return [...new Set(res)];
}

function adjustCodeHeight() {
  // Chrome-only bug (apparently): it doesn't limit the scroller element height
  const scroller = cm.display.scroller;
  const prevWindowHeight = adjustCodeHeight.prevWindowHeight;
  if (scroller.scrollHeight === scroller.clientHeight ||
      prevWindowHeight && window.innerHeight !== prevWindowHeight) {
    adjustCodeHeight.prevWindowHeight = window.innerHeight;
    cm.setSize(null, $('.main').offsetHeight - $('.warnings').offsetHeight);
  }
}

function initLiveReload() {
  const DELAY = 500;
  let isEnabled = false;
  let timer = 0;
  const getData = preinit.getData;
  let sequence = preinit.ready;
  return {
    get enabled() {
      return isEnabled;
    },
    onToggled(e) {
      if (e) isEnabled = e.target.checked;
      if (installed || installedDup) {
        if (isEnabled) {
          check({force: true});
        } else {
          stop();
        }
        $('.install').disabled = isEnabled;
        Object.assign($('#live-reload-install-hint'), {
          hidden: !isEnabled,
          textContent: t(`liveReloadInstallHint${tabId >= 0 ? 'FF' : ''}`),
        });
      }
    },
  };
  function check(opts) {
    getData(opts)
      .then(update, logError)
      .then(() => {
        timer = 0;
        start();
      });
  }
  function logError(error) {
    console.warn(t('liveReloadError', error));
  }
  function start() {
    timer = timer || setTimeout(check, DELAY);
  }
  function stop() {
    clearTimeout(timer);
    timer = 0;
  }
  function update(code) {
    if (code == null) return;
    sequence = sequence.catch(console.error).then(() => {
      const {id} = installed || installedDup;
      const scrollInfo = cm.getScrollInfo();
      const cursor = cm.getCursor();
      cm.setValue(code);
      cm.setCursor(cursor);
      cm.scrollTo(scrollInfo.left, scrollInfo.top);
      return API.usercss.install({id, sourceCode: code})
        .then(updateMeta)
        .catch(showError);
    });
  }
}

function shouldShowConfig() {
  // TODO: rewrite message-box to support multiple instances or find an existing tiny library
  const prev = cfgShown;
  cfgShown = $(CFG_SEL) != null;
  return prev && !cfgShown;
}

function replaceChildren(el, children, toggleParent) {
  if (el.firstChild) el.textContent = '';
  if (children) el.append(...Array.isArray(children) ? children : [children]);
  if (toggleParent) el.parentNode.hidden = !el.firstChild;
}