

import Array_isArray from '@schematize/refs/js/Array.isArray.mjs';
import Array_prototype_forEach_call from '@schematize/refs/js/Array.prototype.forEach.call.mjs';
import Array_prototype_join_call from '@schematize/refs/js/Array.prototype.join.call.mjs';
import Array_prototype_map_call from '@schematize/refs/js/Array.prototype.map.call.mjs';
import Object_create from '@schematize/refs/js/Object.create.mjs';
import Object_keys from '@schematize/refs/js/Object.keys.mjs';
import Object_getPrototypeOf from '@schematize/refs/js/Object.getPrototypeOf.mjs';
import Object_prototype_hasOwnProperty_call from '@schematize/refs/js/Object.prototype.hasOwnProperty.call.mjs';
import Symbol from '@schematize/refs/js/Symbol.mjs';

import on from '@schematize/instance.js/src/EventTarget/on.mjs';
import off from '@schematize/instance.js/src/EventTarget/off.mjs';

import {
  set,
} from './customInstance.mjs';
// import set from '@schematize/instance.js/src/Instance/set.mjs';

import {
  addEventListener,
  preventDefault,
} from './eventTarget.mjs';

import {
  pushState,
} from './customHistory.mjs';

import {
  createElement,
  createElementNS,
  createTextNode,
  createComment,
  append,
  after,
  replaceWith,
  remove,
  setAttribute,
} from './dom.mjs';

const S_function = `function`;

// a collection of dom functions used for building my app

// symbol for linked child objects
const assignScope = (node, parent, scope) => {
  
  // if it is not the root
  if (parent) {

    // get the parent scope
    let parentScope = parent._;

    // if we want a new scope
    if (scope) {

      // get a reference to the parent scope on this scope (a.k.a the _proto_)
      scope._ = parentScope;

      // change to property descriptors
      for (let property in scope) {
        scope[property] = {
          // configurable: true, // don't want this reconfigured
          enumerable: true,
          writable: true,
          value: scope[property],
        };
      }
      // Object_setPrototypeOf(scope, parentScope);
      // This is faster:
      scope = Object_create(parentScope, scope);

    // if we don't care about a new scope, simply reference my parent's scope
    } else {
      scope = parentScope;
    }

  // if it is the root
  } else {
    scope = scope || {};
  }

  // assign the scope to the node/element
  node._ = scope;
};

// createNS
export const createNS = (namespace, name, parent, attributes, children, scope) => {
  let
    options,
    element,
    attribute;

  // extra options to createElement
  if (attributes?.is) {
    options = {
      is: attributes.is
    };
    delete attributes.is;
  }

  // create the element
  if (namespace) {
    element = createElementNS(namespace, name, options);
  } else {
    element = createElement(name, options);
  }

  // set all of the attributes/properties
  if (attributes) {
    for (attribute in attributes) {

      // if it is a property, set it using dot notation, etc.
      if (
        !namespace &&
        (attribute in element)
      ) {
        element[attribute] = attributes[attribute];

      // otherwise set it as an attribute
      } else {
        // properties of things like SVG are able to be set only through
        // setAttribute
        // setAttributeNS(element, namespace, attribute, attributes[attribute]);
        setAttribute(element, attribute, attributes[attribute]);
      }

    }
  }

  // assign scope
  assignScope(element, parent, scope);

  // if it is a function
  if (
    children &&
    typeof children === S_function
  ) {
    // call the function passing the parent for the children to access
    children = children(element);
  }

  // if children array, append them
  if (Array_isArray(children)) {
    // append the children
    append(element, ...children);
  }

  return element;
};
// create
export const create = (...args) => (
  createNS(0, ...args)
);
// createSVG
export const createSVG = (...args) => (
  createNS('http://www.w3.org/2000/svg', ...args)
);
// creator
export const creator = (name) => (
  (...args) => createNS(0, name, ...args)
);
// creatorNS
export const creatorNS = (namespace, name) => (
  (...args) => createNS(namespace, name, ...args)
);
// creatorSVG
export const creatorSVG = (name) => (
  creatorNS('http://www.w3.org/2000/svg', name)
);

// createText
export const createText = (text, parent, fn, scope) => {

  // create the node
  let node = createTextNode(text);

  // assign scope
  assignScope(node, parent, scope);

  if (
    fn &&
    typeof fn === S_function
  ) {
    fn(node);
  }

  return node;

};

// style => create a style tag from an object
export const style = (obj) => Array_prototype_join_call(
  Array_prototype_map_call(
    Object_keys(obj), (key) => (key + ':' + obj[key] + ';')
  ), ''
);

// resolve
export const resolve = (
  obj,
  properties,
  // vars
  property,
  i = 0,
  il = properties.length,
) => {

  for (; i < il; i++) {
    property = properties[i];
    // if array , get property
    if (Array_isArray(property)) {
      property = property[0];
    }
    // special property to signify "any property on the object"
    // so we just return the object
    if (
      property === SYMBOL_ALL_PROPERTIES ||
      property === SYMBOL_ALL_ITEMS
    ) {
      continue;
    }
    // drop into the property
    if (
      obj &&
      property in obj
    ) {
      obj = obj[property];
    // if it doesn't exist, return
    } else {
      return;
    }
  }

  return obj;
};

// createHub
export const createHub = (connections, inits) => {

  // hub
  let hub = {
    connections: connections || []
  };

  // update
  hub.u = (...newValue) => {
    Array_prototype_forEach_call(hub.connections, (connection) => {
      connection(...newValue);
    });
  };

  // initialize all of the connections
  if (inits) {
    Array_prototype_forEach_call(inits, (init) => {
      init(hub.u);
    });
  }

  return hub;
};

export const SYMBOL_ALL_PROPERTIES = Symbol('*');
export const SYMBOL_ALL_ITEMS = Symbol(`[]`);
// listen(_, [`array`, [SYMBOL_ALL_ITEMS, `change`], `array_item_property`, `array_item_property_property`]);

const addDownstreamUnlisteners = (
  downstream,
  obj,
  properties,
  propertyName,
  cb,
  i,
  rootObj,
  // vars
  downstreamObject,
  item,
) => {

  // if this is an array and we want to loop through each item of the array
  // then set up the listeners for all the items in the array
  if (propertyName === SYMBOL_ALL_ITEMS) {

    // make sure that the obj is really an array
    if (!Array_isArray(obj)) {
      throw Error(`SYMBOL_ALL_ITEMS only for Arrays`);
    }

    // loop through each item and listen to it's properties
    for (item of obj) {
      downstream.push(
        listen(item, properties, cb, 0, i, rootObj)
      );
    }

  // otherwise it should be a regular property of an object-like value
  } else {

    if ((downstreamObject = obj[propertyName])) {
      downstream.push(
        listen(downstreamObject, properties, cb, 0, i, rootObj)
      );
    }

  }

};

const removeDownstreamUnlisteners = (
  downstream,
  // vars
  i,
) => {
  for (i = downstream.length - 1; i >= 0; i--) {
    downstream[i]();
    downstream.splice(i, 1);
  }
};

// listen
export const listen = (
  obj,
  properties,
  cb,
  fireImmediately,
  i = 0,
  rootObj,

  // vars
  proto,
  property,
  propertyName,
  eventName,
  downstream = [],
  fn,
) => {

  // remove the first value from the array (the next item in the list)
  property = properties[i];
  propertyName = property;
  eventName = listen.eventName;
  if (Array_isArray(property)) {
    propertyName = property[0];
    eventName = property[1];
  }

  // we haven't yet derived the rootObj (This is the first 
  // property/object)
  if (!rootObj) {

    // NOTE: This code is mean to be here to give us a major performance 
    // improvement.  It is meant to figure out which scope actually owns the 
    // property and then listen for changes on that object.  If we didn't have
    // this, it would mean that we would have to dispatch events on the object 
    // and then propogate them to all of the children, and all of their children
    // etc.
    rootObj = obj;

    // if it's the special all properties symbol, use the root object
    if (propertyName === SYMBOL_ALL_PROPERTIES) {
      // use the root object as the object
      // NOTE: We can skip since obj is already the root object and vice versa
      // obj = rootObj;
    // otherwise check for which object owns the property
    } else {
      // traverse up the __proto__ chain
      while (
        !Object_prototype_hasOwnProperty_call(obj, propertyName) && 
        (proto = Object_getPrototypeOf(obj))
      ) {
        obj = proto;
      }
      // final check and revert if necessary
      if (!Object_prototype_hasOwnProperty_call(obj, propertyName)) {
        obj = rootObj;
      }
    }

    // set the root object
    rootObj = obj;
  }

  // update downstream listeners
  i < properties.length - 1 &&
  addDownstreamUnlisteners(downstream, obj, properties, propertyName, cb, i+1, rootObj);

  fn = (ev) => {

    // special property to signify "any property on the object"
    if (propertyName === SYMBOL_ALL_PROPERTIES) {
      // NOTE: We don't need to remove downstream listeners because this is 
      // the end of the line
      // make sure to update with the latest information
      cb(resolve(rootObj, properties), ev);

    // only action whenever it is that exact propertyName
    // NOTE: For different events, we get different event.detail properties.
    } else if (
      // if it's all items in the array, and the array just changed...
      (
        propertyName === SYMBOL_ALL_ITEMS
      ) ||
      // "modify" event
      (
        eventName === `modify` &&
        // NOTE: double equals because for array properties, the index can come 
        // through as an integer or a string
        ev.detail.property == propertyName
      ) || 
      // "change" event
      (
        eventName === `change` && 
        ev.detail.changes?.has?.(propertyName)
      )
    ) {
      // stop listening to downstream properties since it just got updated
      removeDownstreamUnlisteners(downstream);
      // update downstream listeners
      i < properties.length - 1 &&
      addDownstreamUnlisteners(downstream, obj, properties, propertyName, cb, i+1, rootObj);
      // I was the one the got the event, so I need to make sure to 
      // call the callback with the latest value
      cb(resolve(rootObj, properties), ev);
    }

  };

  // listen to me
  on(eventName, fn, obj);

  // if we should fire the event immediately
  if (fireImmediately) {
    cb(resolve(rootObj, properties), {
      type: `__init__`,
      detail: {}
    });
  }

  // return a way for my parent to remove listeners if it is updated
  return () => {
    // remove my own listener
    off(eventName, fn, obj);
    // remove any listeners from my children
    removeDownstreamUnlisteners(downstream);
  };

};
listen.eventName = `modify`;

export const conditional = (element, inits) => {
  let 
    comment = createComment('conditional'),
    isCommentShown = true;
  
  createHub([
    (shouldShow) => {
      
      // NOTE: these conditions should help us to ensure 
      // that we don't try to replace something that isn't there 
      // in the first place.  Like calling the updater with "true" multiple 
      // times in a row for example. 
      
      // only replace when the comment is shown
      if (
        isCommentShown &&
        shouldShow
      ) {
        replaceWith(comment, element);
        isCommentShown = false;
      
      // only replace when the element is shown
      } else if (
        !shouldShow &&
        !isCommentShown
      ) {
        replaceWith(element, comment);
        isCommentShown = true;
      }
      
    }
  ], [
    ...inits  
  ]);
  
  return comment;
};

// toggle
// condition = a callback that when returns false will choose element1, if true
// will return element2
export const toggle = (element1, element2, condition, inits) => {
  let
    current,
    toggle,
    update;

  // update the currently implemented element, and replace the current element
  // if it isn't
  update = (element) => {
    if (current !== element) {
      if (current) {
        replaceWith(current, element);
      }
      current = element;
    }
  };

  // callback to call when an update has been made
  toggle = () => {

    // if true, choose element1 to display
    if (condition()) {
      update(element1);
    // if false, choose element2 to display
    } else {
      update(element2);
    }

    return current;
  };

  // initialize all of the connections
  if (inits) {
    Array_prototype_forEach_call(inits, (init) => {
      init(toggle);
    });
  }

  return toggle();
};

const reIndex = (
  nodeList,
  i,
  indexProperty,
  node,
  _,
) => {
  for (; i < nodeList.length; i++) {
    if (
      (node = nodeList[i]) &&
      // NOTE: not checking for anchorStart or anchorEnd because they never 
      // have a scope "_"
      (_ = node._) &&
      _[indexProperty] !== i
    ) {
      set(_, indexProperty, i);
    }
  }
};

let repeatingGroupNumber = 0;
export const repeat = (
  init,
  childGenerator,
  scopeProperty = 'item', 
  indexProperty,

  // vars
  nodeList = [],
  hasRendered = false,
  // repeat number
  repeatNum = ++repeatingGroupNumber,
  // create the anchors to start and end the nodeList
  anchorStart = createComment(`repeat-${repeatNum}`),
  anchorEnd = createComment(`repeat-${repeatNum}`),
  // init args 
  update,
  removeItem,
  insertItem,
  updateItem,
) => {

  removeItem = (
    i,
    // vars
    n = i + 1,
    node = nodeList[n],
    name = indexProperty,
  ) => {
    if (
      node &&
      node !== anchorStart && 
      node !== anchorEnd
    ) {
      nodeList.splice(n, 1);
      hasRendered && remove(node);

      if (name) {
        reIndex(nodeList, i, name);
      }
    }
  };

  insertItem = (
    i,
    item,
    // vars
    n = i + 1,
    name = indexProperty,
    node,
  ) => {
    node = childGenerator(item, i);
    nodeList.splice(n, 0, node);
    hasRendered && after(nodeList[i], node);

    if (name) {
      reIndex(nodeList, i, name);
    }
  };

  updateItem = (
    i,
    item,
    // vars
    n = i + 1,
    node = nodeList[n],
    _ = node?._,
    name = indexProperty,
  ) => {
    if (node) {
      // update the item
      set(_, scopeProperty, item);

      // update the index
      name && 
      (_[name] !== i) &&
      set(_, name, i);
    }
  };

  // update the nodeList
  update = (
    items,

    // vars
    item,
    index,
    indexIncrement,
    i, il, n,
    node, _, v,
    nextNode, next_,
  ) => {

    // add anchor start
    nodeList[0] !== anchorStart && 
    (nodeList[0] = anchorStart);

    // index is the index scope property (when applicable by "indexProperty")
    index = 0;
    // n is the index of the node in the nodeList (these are different because 
    // the nodeList has an anchor start and anchor end)
    // for (item of items) {
    // n is the index of the node in the nodeList (these are different because 
    // the nodeList has an anchor start and anchor end)
    n = 1;
    // i is the index of the item in the items array
    for (i = 0, il = items.length; i < il; i++, n++) {
      item = items[i];
      // don't increment index
      indexIncrement = 1;

      // dynamically determine if I should:
      // a) skip the current item (because the scope is already set to the correct item)
      // b) add a new node
      // c) insert a new node in between
      // d) delete the current node
      // e) update/exchange the existing node scope

      // get the current node
      node = nodeList[n];
      _ = node?._;
      v = _?.[scopeProperty];

      // a) skip the current item (because the scope is already set to the correct item)
      if (v === item) {

        if (
          indexProperty &&
          _[indexProperty] !== index
        ) {
          set(_, indexProperty, index);
        }

      // b) add a new node
      } else if (
        // if we haven't added it to the node list yet
        !node || 
        // or if we've reached the end of the node list
        node === anchorEnd
      ) {
        node = nodeList[n] = childGenerator(item, i);
        hasRendered && after(nodeList[i], node);

      // c) insert a new node in between
      } else if (
        // or if the current node's scope is the next item in the list
        v && 
        items[n] &&
        v === items[n]
      ) {
        insertItem(i, item, n, 0);

      // d) delete the current node
      } else if (
        // if the next node's scope is the current item
        (nextNode = nodeList[n + 1]) &&
        (next_ = nextNode._) &&
        next_?.[scopeProperty] === item
      ) {
        removeItem(i, n, node, 0);

        // NOTE: don't increment index here because we are removing the node
        indexIncrement = 0;

      // e) update the existing node scope
      } else {
        updateItem(i, item, n, node, _);
      }

      index += indexIncrement;
    }

    // remove the rest of the nodes (in reverse order for performance)
    i = nodeList.length - 1;
    // NOTE: the lowest "n" can be here is 1, so we don't need to check for
    // anchorStart
    while (i >= n) {
      node = nodeList.pop();
      hasRendered && 
      node !== anchorEnd && 
      remove(node);
      // decrement
      --i;
    }

    // add anchor end
    nodeList[n] = anchorEnd;
  };

  // if we were passed an initialization function, call it with the update
  // function, so we can listen for any changes whenever the items change
  if (init) {
    init(update, removeItem, insertItem, updateItem);
  }

  hasRendered = true;

  // add start anchor to beginning of nodeList
  (nodeList[0] !== anchorStart) &&
  nodeList.unshift(anchorStart);

  // add end anchor to the end of the nodeList
  (nodeList[nodeList.length - 1] !== anchorEnd) &&
  nodeList.push(anchorEnd);

  // return the nodeList
  return nodeList;
};

// link
// pass in a link element and a SPA url and we will make sure that we get you
// to the right place
export const link = (a, url) => {
  addEventListener(a, 'click', (ev) => {
    preventDefault(ev);
    pushState(null, '', url);
  });
};

// onBecameVisible
export const onBecameVisible = (element, cb) => {
  let observer = new IntersectionObserver((entries) => {
    let last;
    Array_prototype_forEach_call(entries, (entry) => (
      (!last || (entry.time > last.time)) && (last = entry)
    ));
    // if the last entry says we are intersecting, then fire the callback
    if (last.isIntersecting) {
      cb(last);
    }
  });
  observer.observe(element);
  return element;
};