const EMPTY_ARRAY = Object.freeze([]); // import Logger from './logger';
// let alreadyWarned = false;

function debugAssert(test, msg) {
  // if (!alreadyWarned) {
  //   alreadyWarned = true;
  //   Logger.warn("Don't leave debug assertions on in public builds");
  // }
  if (!test) {
    throw new Error(msg || 'assertion failure');
  }
}

function deprecate(desc) {
  console.warn(`DEPRECATION: ${desc}`);
}

let GUID = 0;

function initializeGuid(object) {
  return object._guid = ++GUID;
}

function ensureGuid(object) {
  return object._guid || initializeGuid(object);
}

function dict() {
  return Object.create(null);
}

function isDict(u) {
  return u !== null && u !== undefined;
}

function isObject(u) {
  return typeof u === 'object' && u !== null;
}

class DictSet {
  constructor() {
    this.dict = dict();
  }

  add(obj) {
    if (typeof obj === 'string') this.dict[obj] = obj;else this.dict[ensureGuid(obj)] = obj;
    return this;
  }

  delete(obj) {
    if (typeof obj === 'string') delete this.dict[obj];else if (obj._guid) delete this.dict[obj._guid];
  }

}

class StackImpl {
  constructor() {
    this.stack = [];
    this.current = null;
  }

  get size() {
    return this.stack.length;
  }

  push(item) {
    this.current = item;
    this.stack.push(item);
  }

  pop() {
    let item = this.stack.pop();
    let len = this.stack.length;
    this.current = len === 0 ? null : this.stack[len - 1];
    return item === undefined ? null : item;
  }

  nth(from) {
    let len = this.stack.length;
    return len < from ? null : this.stack[len - from];
  }

  isEmpty() {
    return this.stack.length === 0;
  }

  toArray() {
    return this.stack;
  }

}

function keys(obj) {
  return Object.keys(obj);
}

function unwrap(val) {
  if (val === null || val === undefined) throw new Error(`Expected value to be present`);
  return val;
}

function expect(val, message) {
  if (val === null || val === undefined) throw new Error(message);
  return val;
}

function unreachable(message = 'unreachable') {
  return new Error(message);
}

function exhausted(value) {
  throw new Error(`Exhausted ${value}`);
}

const tuple = (...args) => args;

const symbol = typeof Symbol !== 'undefined' ? Symbol : key => `__${key}${Math.floor(Math.random() * Date.now())}__`;
const DESTROY = symbol('DESTROY');

function isDestroyable(value) {
  return !!(value && value[DESTROY] !== undefined);
}

function isStringDestroyable(value) {
  return !!(value && typeof value === 'object' && typeof value.destroy === 'function');
}

function clearElement(parent) {
  let current = parent.firstChild;

  while (current) {
    let next = current.nextSibling;
    parent.removeChild(current);
    current = next;
  }
}

const SERIALIZATION_FIRST_NODE_STRING = '%+b:0%';

function isSerializationFirstNode(node) {
  return node.nodeValue === SERIALIZATION_FIRST_NODE_STRING;
}

const LINKED = new WeakMap();
const WILL_DROP = symbol('WILL_DROP');
const DID_DROP = symbol('DID_DROP');
const CHILDREN = symbol('CHILDREN');
const DESTRUCTORS = new WeakMap();

function isDrop(value) {
  if (value === null || typeof value !== 'object') return false;
  return value[DID_DROP] !== undefined;
}

function associate(parent, child) {
  associateDestructor(parent, destructor(child));
}

function associateDestructor(parent, child) {
  let associated = LINKED.get(parent);

  if (!associated) {
    associated = new Set();
    LINKED.set(parent, associated);
  }

  associated.add(child);
}

function peekAssociated(parent) {
  return LINKED.get(parent) || null;
}

function takeAssociated(parent) {
  let linked = LINKED.get(parent);

  if (linked && linked.size > 0) {
    LINKED.delete(parent);
    return linked;
  } else {
    return null;
  }
}

function willDestroyAssociated(parent) {
  let associated = LINKED.get(parent);

  if (associated) {
    associated.forEach(item => {
      item[WILL_DROP]();
    });
  }
}

function didDestroyAssociated(parent) {
  let associated = LINKED.get(parent);

  if (associated) {
    associated.forEach(item => {
      item[DID_DROP]();
      associated.delete(item);
    });
  }
}

function destructor(value) {
  let d = DESTRUCTORS.get(value);

  if (!d) {
    if (isDestroyable(value)) {
      d = new DestroyableDestructor(value);
    } else if (isStringDestroyable(value)) {
      d = new StringDestroyableDestructor(value);
    } else {
      d = new SimpleDestructor(value);
    }

    DESTRUCTORS.set(value, d);
  }

  return d;
}

function snapshot(values) {
  return new SnapshotDestructor(values);
}

class SnapshotDestructor {
  constructor(destructors) {
    this.destructors = destructors;
  }

  [WILL_DROP]() {
    this.destructors.forEach(item => item[WILL_DROP]());
  }

  [DID_DROP]() {
    this.destructors.forEach(item => item[DID_DROP]());
  }

  get [CHILDREN]() {
    return this.destructors;
  }

  toString() {
    return 'SnapshotDestructor';
  }

}

class DestroyableDestructor {
  constructor(inner) {
    this.inner = inner;
  }

  [WILL_DROP]() {
    willDestroyAssociated(this.inner);
  }

  [DID_DROP]() {
    this.inner[DESTROY]();
    didDestroyAssociated(this.inner);
  }

  get [CHILDREN]() {
    return LINKED.get(this.inner) || [];
  }

  toString() {
    return 'DestroyableDestructor';
  }

}

class StringDestroyableDestructor {
  constructor(inner) {
    this.inner = inner;
  }

  [WILL_DROP]() {
    if (typeof this.inner.willDestroy === 'function') {
      this.inner.willDestroy();
    }

    willDestroyAssociated(this.inner);
  }

  [DID_DROP]() {
    this.inner.destroy();
    didDestroyAssociated(this.inner);
  }

  get [CHILDREN]() {
    return LINKED.get(this.inner) || [];
  }

  toString() {
    return 'StringDestroyableDestructor';
  }

}

class SimpleDestructor {
  constructor(inner) {
    this.inner = inner;
  }

  [WILL_DROP]() {
    willDestroyAssociated(this.inner);
  }

  [DID_DROP]() {
    didDestroyAssociated(this.inner);
  }

  get [CHILDREN]() {
    return LINKED.get(this.inner) || [];
  }

  toString() {
    return 'SimpleDestructor';
  }

}

class ListContentsDestructor {
  constructor(inner) {
    this.inner = inner;
  }

  [WILL_DROP]() {
    this.inner.forEachNode(d => destructor(d)[WILL_DROP]());
  }

  [DID_DROP]() {
    this.inner.forEachNode(d => destructor(d)[DID_DROP]());
  }

  get [CHILDREN]() {
    let out = [];
    this.inner.forEachNode(d => out.push(...destructor(d)[CHILDREN]));
    return out;
  }

  toString() {
    return 'ListContentsDestructor';
  }

}

function debugDropTree(inner) {
  let hasDrop = isDrop(inner);
  let rawChildren = LINKED.get(inner) || null;
  let children = null;

  if (rawChildren) {
    children = [];

    for (let child of rawChildren) {
      children.push(debugDropTree(child));
    }
  }

  let obj = Object.create(null);
  obj.inner = inner;

  if (children) {
    obj.children = children;
  }

  obj.hasDrop = hasDrop;
  return obj;
}

function printDropTree(inner) {
  printDrop(destructor(inner));
}

function printDrop(inner) {
  console.group(String(inner));
  console.log(inner);
  let children = inner[CHILDREN] || null;

  if (children) {
    for (let child of children) {
      printDrop(child);
    }
  }

  console.groupEnd();
}

class ListNode {
  constructor(value) {
    this.next = null;
    this.prev = null;
    this.value = value;
  }

}

class LinkedList {
  constructor() {
    this.clear();
  }

  head() {
    return this._head;
  }

  tail() {
    return this._tail;
  }

  clear() {
    this._head = this._tail = null;
  }

  toArray() {
    let out = [];
    this.forEachNode(n => out.push(n));
    return out;
  }

  nextNode(node) {
    return node.next;
  }

  forEachNode(callback) {
    let node = this._head;

    while (node !== null) {
      callback(node);
      node = node.next;
    }
  }

  insertBefore(node, reference = null) {
    if (reference === null) return this.append(node);
    if (reference.prev) reference.prev.next = node;else this._head = node;
    node.prev = reference.prev;
    node.next = reference;
    reference.prev = node;
    return node;
  }

  append(node) {
    let tail = this._tail;

    if (tail) {
      tail.next = node;
      node.prev = tail;
      node.next = null;
    } else {
      this._head = node;
    }

    return this._tail = node;
  }

  remove(node) {
    if (node.prev) node.prev.next = node.next;else this._head = node.next;
    if (node.next) node.next.prev = node.prev;else this._tail = node.prev;
    return node;
  }

  [WILL_DROP]() {
    this.forEachNode(d => destructor(d)[WILL_DROP]());
  }

  [DID_DROP]() {
    this.forEachNode(d => destructor(d)[DID_DROP]());
  }

  get [CHILDREN]() {
    let out = [];
    this.forEachNode(d => out.push(...destructor(d)[CHILDREN]));
    return out;
  }

}

class ListSlice {
  constructor(head, tail) {
    this._head = head;
    this._tail = tail;
  }

  forEachNode(callback) {
    let node = this._head;

    while (node !== null) {
      callback(node);
      node = this.nextNode(node);
    }
  }

  head() {
    return this._head;
  }

  tail() {
    return this._tail;
  }

  toArray() {
    let out = [];
    this.forEachNode(n => out.push(n));
    return out;
  }

  nextNode(node) {
    if (node === this._tail) return null;
    return node.next;
  }

}

const EMPTY_SLICE = new ListSlice(null, null);
const {
  keys: objKeys
} = Object;

function assign(obj) {
  for (let i = 1; i < arguments.length; i++) {
    let assignment = arguments[i];
    if (assignment === null || typeof assignment !== 'object') continue;
    let keys = objKeys(assignment);

    for (let j = 0; j < keys.length; j++) {
      let key = keys[j];
      obj[key] = assignment[key];
    }
  }

  return obj;
}

function fillNulls(count) {
  let arr = new Array(count);

  for (let i = 0; i < count; i++) {
    arr[i] = null;
  }

  return arr;
}

function values(obj) {
  const vals = [];

  for (const key in obj) {
    vals.push(obj[key]);
  }

  return vals;
}

function strip(strings, ...args) {
  let out = '';

  for (let i = 0; i < strings.length; i++) {
    let string = strings[i];
    let dynamic = args[i] !== undefined ? String(args[i]) : '';
    out += `${string}${dynamic}`;
  }

  let lines = out.split('\n');

  while (lines.length && lines[0].match(/^\s*$/)) {
    lines.shift();
  }

  while (lines.length && lines[lines.length - 1].match(/^\s*$/)) {
    lines.pop();
  }

  let min = Infinity;

  for (let line of lines) {
    let leading = line.match(/^\s*/)[0].length;
    min = Math.min(min, leading);
  }

  let stripped = [];

  for (let line of lines) {
    stripped.push(line.slice(min));
  }

  return stripped.join('\n');
}

function assertNever(value, desc = 'unexpected unreachable branch') {
  console.log('unreachable', value);
  console.trace(`${desc} :: ${JSON.stringify(value)} (${value})`);
}

export { assertNever, EMPTY_ARRAY, debugAssert as assert, deprecate, dict, DictSet, isDict, isObject, StackImpl as Stack, ensureGuid, initializeGuid, isSerializationFirstNode, SERIALIZATION_FIRST_NODE_STRING, EMPTY_SLICE, LinkedList, ListNode, ListSlice, assign, fillNulls, values, DESTROY, isDestroyable, isStringDestroyable, clearElement, LINKED, WILL_DROP, DID_DROP, CHILDREN, DESTRUCTORS, isDrop, associate, associateDestructor, peekAssociated, takeAssociated, willDestroyAssociated, didDestroyAssociated, destructor, snapshot, ListContentsDestructor, debugDropTree, printDropTree, printDrop, keys, unwrap, expect, unreachable, exhausted, tuple, symbol, strip };