import * as localWasm from '../../wasm';
import { EditorView, basicSetup } from 'codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { css } from '@codemirror/lang-css';
import { oneDark } from '@codemirror/theme-one-dark';
import { syntaxTree } from '@codemirror/language';
import { linter, lintGutter } from '@codemirror/lint'
import { Compartment } from '@codemirror/state'

const linterCompartment = new Compartment;
const visitorLinterCompartment = new Compartment;

let wasm;
let editor, visitorEditor, outputEditor, modulesEditor, depsEditor;
let enc = new TextEncoder();
let dec = new TextDecoder();
let inputs = document.querySelectorAll('input[type=number]');

async function loadVersions() {
  const { versions } = await fetch('https://data.jsdelivr.com/v1/package/npm/lightningcss-wasm').then(r => r.json());
  versions
    .map(v => {
      const option = document.createElement('option');
      option.value = v;
      option.textContent = v;
      return option;
    })
    .forEach(o => {
      version.appendChild(o);
    })
}

async function loadWasm() {
  if (version.value === 'local') {
    wasm = localWasm;
  } else {
    wasm = await new Function('version', 'return import(`https://esm.sh/lightningcss-wasm@${version}?bundle`)')(version.value);
  }
  await wasm.default();
}

function loadPlaygroundState() {
  const hash = window.location.hash.slice(1);
  try {
    return JSON.parse(decodeURIComponent(hash));
  } catch {
    return {
      minify: minify.checked,
      visitorEnabled: visitorEnabled.checked,
      targets: getTargets(),
      include: 0,
      exclude: 0,
      source: `@custom-media --modern (color), (hover);

.foo {
  background: yellow;

  -webkit-border-radius: 2px;
  -moz-border-radius: 2px;
  border-radius: 2px;

  -webkit-transition: background 200ms;
  -moz-transition: background 200ms;
  transition: background 200ms;

  &.bar {
    color: green;
  }
}

@media (--modern) and (width > 1024px) {
  .a {
    color: green;
  }
}`,
      version: version.value,
      visitor: `{
  Color(color) {
    if (color.type === 'rgb') {
      color.g = 0;
      return color;
    }
  }
}`
    };
  }
}

function reflectPlaygroundState(playgroundState) {
  if (typeof playgroundState.minify !== 'undefined') {
    minify.checked = playgroundState.minify;
  }

  if (typeof playgroundState.cssModules !== 'undefined') {
    cssModules.checked = playgroundState.cssModules;
    compiledModules.hidden = !playgroundState.cssModules;
  }

  if (typeof playgroundState.analyzeDependencies !== 'undefined') {
    analyzeDependencies.checked = playgroundState.analyzeDependencies;
    compiledDependencies.hidden = !playgroundState.analyzeDependencies;
  }

  if (typeof playgroundState.customMedia !== 'undefined') {
    customMedia.checked = playgroundState.customMedia;
  }

  if (typeof playgroundState.scrollNavigationControls !== 'undefined') {
    scrollNavigationControls.checked = playgroundState.scrollNavigationControls;
  }

  if (typeof playgroundState.visitorEnabled !== 'undefined') {
    visitorEnabled.checked = playgroundState.visitorEnabled;
  }

  if (playgroundState.targets) {
    const { targets } = playgroundState;
    for (let input of inputs) {
      let value = targets[input.id];
      input.value = value == null ? '' : value >> 16;
    }
  }

  updateFeatures(sidebar.elements.include, playgroundState.include);
  updateFeatures(sidebar.elements.exclude, playgroundState.exclude);
  if (playgroundState.include) {
    include.parentElement.open = true;
  }
  if (playgroundState.exclude) {
    exclude.parentElement.open = true;
  }

  if (playgroundState.unusedSymbols) {
    unusedSymbols.value = playgroundState.unusedSymbols.join('\n');
  }

  if (playgroundState.version) {
    version.value = playgroundState.version;
  }
}

function savePlaygroundState() {
  let data = new FormData(sidebar);
  const playgroundState = {
    minify: minify.checked,
    customMedia: customMedia.checked,
    scrollNavigationControls: scrollNavigationControls.checked,
    cssModules: cssModules.checked,
    analyzeDependencies: analyzeDependencies.checked,
    targets: getTargets(),
    include: getFeatures(data.getAll('include')),
    exclude: getFeatures(data.getAll('exclude')),
    source: editor.state.doc.toString(),
    visitorEnabled: visitorEnabled.checked,
    visitor: visitorEditor.state.doc.toString(),
    unusedSymbols: unusedSymbols.value.split('\n').map(v => v.trim()).filter(Boolean),
    version: version.value,
  };

  const hash = encodeURIComponent(JSON.stringify(playgroundState));

  if (
    typeof URL === 'function' &&
    typeof history === 'object' &&
    typeof history.replaceState === 'function'
  ) {
    const url = new URL(location.href);
    url.hash = hash;
    history.replaceState(null, null, url);
  } else {
    location.hash = hash;
  }
}

function getTargets() {
  let targets = {};
  for (let input of inputs) {
    if (input.value !== '') {
      targets[input.id] = input.valueAsNumber << 16;
    }
  }

  return targets;
}

function getFeatures(vals) {
  let features = 0;
  for (let name of vals) {
    features |= wasm.Features[name];
  }
  return features;
}

function updateFeatures(elements, include) {
  for (let checkbox of elements) {
    let feature = wasm.Features[checkbox.value];
    checkbox.checked = (include & feature) === feature;
    checkbox.indeterminate = !checkbox.checked && (include & feature);
  }
}

function update() {
  const { transform } = wasm;

  const targets = getTargets();
  let data = new FormData(sidebar);
  let include = getFeatures(data.getAll('include'));
  let exclude = getFeatures(data.getAll('exclude'));
  try {
    let res = transform({
      filename: 'test.css',
      code: enc.encode(editor.state.doc.toString()),
      minify: minify.checked,
      targets: Object.keys(targets).length === 0 ? null : targets,
      include,
      exclude,
      drafts: {
        customMedia: customMedia.checked,
        scrollNavigationControls: scrollNavigationControls.checked
      },
      cssModules: cssModules.checked,
      analyzeDependencies: analyzeDependencies.checked,
      unusedSymbols: unusedSymbols.value.split('\n').map(v => v.trim()).filter(Boolean),
      visitor: visitorEnabled.checked ? (0, eval)('(' + visitorEditor.state.doc.toString() + ')') : undefined,
    });

    let update = outputEditor.state.update({ changes: { from: 0, to: outputEditor.state.doc.length, insert: dec.decode(res.code) } });
    outputEditor.update([update]);

    if (res.exports) {
      let update = modulesEditor.state.update({ changes: { from: 0, to: modulesEditor.state.doc.length, insert: '// CSS module exports\n' + JSON.stringify(res.exports, false, 2) } });
      modulesEditor.update([update]);
    }

    if (res.dependencies) {
      let update = depsEditor.state.update({ changes: { from: 0, to: depsEditor.state.doc.length, insert: '// Dependencies\n' + JSON.stringify(res.dependencies, false, 2) } });
      depsEditor.update([update]);
    }

    compiledModules.hidden = !cssModules.checked;
    compiledDependencies.hidden = !analyzeDependencies.checked;
    visitor.hidden = !visitorEnabled.checked;
    source.dataset.expanded = visitor.hidden;
    compiled.dataset.expanded = compiledModules.hidden && compiledDependencies.hidden;
    compiledModules.dataset.expanded = compiledDependencies.hidden;
    compiledDependencies.dataset.expanded = compiledModules.hidden;

    editor.dispatch({
      effects: linterCompartment.reconfigure(createCssLinter(null, res.warnings))
    });

    visitorEditor.dispatch({
      effects: visitorLinterCompartment.reconfigure(createVisitorLinter(null))
    });
  } catch (e) {
    let update = outputEditor.state.update({ changes: { from: 0, to: outputEditor.state.doc.length, insert: `/* ERROR: ${e.message} */` } });
    outputEditor.update([update]);

    editor.dispatch({
      effects: linterCompartment.reconfigure(createCssLinter(e))
    });

    visitorEditor.dispatch({
      effects: visitorLinterCompartment.reconfigure(createVisitorLinter(e))
    });
  }

  savePlaygroundState();
}

function createCssLinter(lastError, warnings) {
  return linter(view => {
    let diagnostics = [];
    if (lastError && lastError.loc) {
      let l = view.state.doc.line(lastError.loc.line);
      let loc = l.from + lastError.loc.column - 1;
      let node = syntaxTree(view.state).resolveInner(loc, 1);
      diagnostics.push(
        {
          from: node.from,
          to: node.to,
          message: lastError.message,
          severity: 'error'
        }
      );
    }
    if (warnings) {
      for (let warning of warnings) {
        let l = view.state.doc.line(warning.loc.line);
        let loc = l.from + warning.loc.column - 1;
        let node = syntaxTree(view.state).resolveInner(loc, 1);
        diagnostics.push({
          from: node.from,
          to: node.to,
          message: warning.message,
          severity: 'warning'
        });
      }
    }
    return diagnostics;
  }, { delay: 0 });
}

function createVisitorLinter(lastError) {
  return linter(view => {
    if (lastError && !lastError.loc) {
      // Firefox has lineNumber and columnNumber, Safari has line and column.
      let line = lastError.lineNumber ?? lastError.line;
      let column = lastError.columnNumber ?? lastError.column;
      if (lastError.column != null) {
        column--;
      }

      if (line == null) {
        // Chrome.
        let match = lastError.stack.match(/(?:(?:eval.*<anonymous>:)|(?:eval:))(?<line>\d+):(?<column>\d+)/);
        if (match) {
          line = Number(match.groups.line);
          column = Number(match.groups.column);

          // Chrome's column numbers are off by the amount of leading whitespace in the line.
          let l = view.state.doc.line(line);
          let m = l.text.match(/^\s*/);
          if (m) {
            column += m[0].length;
          }
        }
      }

      if (line != null) {
        let l = view.state.doc.line(line);
        let loc = l.from + column;
        let node = syntaxTree(view.state).resolveInner(loc, -1);
        return [
          {
            from: node.from,
            to: node.to,
            message: lastError.message,
            severity: 'error'
          }
        ];
      }
    }
    return [];
  }, { delay: 0 });
}

function renderFeatures(parent, name) {
  for (let feature in wasm.Features) {
    let label = document.createElement('label');
    let checkbox = document.createElement('input');
    checkbox.type = 'checkbox';
    checkbox.name = name;
    checkbox.value = feature;
    checkbox.oninput = () => {
      let data = new FormData(sidebar);
      let flags = getFeatures(data.getAll(name));
      let f = wasm.Features[feature];
      if (checkbox.checked) {
        flags |= f;
      } else {
        flags &= ~f;
      }
      updateFeatures(sidebar.elements[name], flags);
    };
    label.appendChild(checkbox);
    label.appendChild(document.createTextNode(' ' + feature))
    parent.appendChild(label);
  }
}

async function main() {
  await loadWasm();
  renderFeatures(include, 'include');
  renderFeatures(exclude, 'exclude');

  let state = loadPlaygroundState();
  reflectPlaygroundState(state);

  editor = new EditorView({
    extensions: [lintGutter(), basicSetup, css(), oneDark, linterCompartment.of(createCssLinter())],
    parent: source,
    doc: state.source,
    dispatch(tr) {
      editor.update([tr]);
      if (tr.docChanged) {
        update();
      }
    }
  });

  visitorEditor = new EditorView({
    extensions: [lintGutter(), basicSetup, javascript(), oneDark, visitorLinterCompartment.of(createVisitorLinter())],
    parent: visitor,
    doc: state.visitor,
    dispatch(tr) {
      visitorEditor.update([tr]);
      if (tr.docChanged) {
        update();
      }
    }
  });

  outputEditor = new EditorView({
    extensions: [basicSetup, css(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping],
    parent: compiled,
  });

  modulesEditor = new EditorView({
    extensions: [basicSetup, javascript(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping],
    parent: compiledModules,
  });

  depsEditor = new EditorView({
    extensions: [basicSetup, javascript(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping],
    parent: compiledDependencies,
  });

  update();
  sidebar.oninput = update;

  await loadVersions();
  version.onchange = async () => {
    await loadWasm();
    update();
  };
}

main();
