import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import {
  setLineageOfDocs,
  getAncestry,
  renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import { union } from 'lodash';

const insertPropertyFromLibraryNode = new ValidatedMethod({
  name: 'creatureProperties.insertPropertyFromLibraryNode',
  validate: new SimpleSchema({
    nodeIds: {
      type: Array,
      max: 20,
    },
    'nodeIds.$': {
      type: String,
      regEx: SimpleSchema.RegEx.Id,
    },
    parentRef: {
      type: RefSchema,
    },
    order: {
      type: Number,
      optional: true,
    },
  }).validator(),
  mixins: [RateLimiterMixin],
  rateLimit: {
    numRequests: 5,
    timeInterval: 5000,
  },
  run({ nodeIds, parentRef, order }) {
    // get the new ancestry for the properties
    let { parentDoc, ancestors } = getAncestry({ parentRef });

    // Check permission to edit
    let rootCreature;
    if (parentRef.collection === 'creatures') {
      rootCreature = parentDoc;
    } else if (parentRef.collection === 'creatureProperties') {
      rootCreature = getRootCreatureAncestor(parentDoc);
    } else {
      throw `${parentRef.collection} is not a valid parent collection`
    }
    assertEditPermission(rootCreature, this.userId);

    // {libraryId: hasViewPermission}
    //let libraryPermissionMemoir = {};
    let node;
    nodeIds.forEach(nodeId => {
      // TODO: Check library view permission for each node before starting
      node = insertPropertyFromNode(nodeId, ancestors, order);
    });

    // get one of the root inserted docs
    let rootId = node._id;

    // Tree structure changed by inserts, reorder the tree
    reorderDocs({
      collection: CreatureProperties,
      ancestorId: rootCreature._id,
    });
    // Return the docId of the last property, the inserted root property
    return rootId;
  },
});

function insertPropertyFromNode(nodeId, ancestors, order) {
  // Fetch the library node and its decendents, provided they have not been
  // removed
  // TODO: Check permission to read the library this node is in
  let node = LibraryNodes.findOne({
    _id: nodeId,
    removed: { $ne: true },
  });
  if (!node) {
    if (Meteor.isClient) return;
    else {
      throw new Meteor.Error(
        'Insert property from library failed',
        `No library document with id '${nodeId}' was found`
      );
    }
  }
  let oldParent = node.parent;
  let nodes = LibraryNodes.find({
    'ancestors.id': nodeId,
    removed: { $ne: true },
  }).fetch();


  // The root node is first in the array of nodes
  // It must get the first generated ID to prevent flickering
  nodes = [node, ...nodes];

  // Convert all references into actual nodes
  nodes = reifyNodeReferences(nodes);
  // Refetch the root node, it might have been reified
  node = nodes[0] || node;

  // set libraryNodeIds
  storeLibraryNodeReferences(nodes);

  // re-map all the ancestors
  setLineageOfDocs({
    docArray: nodes,
    newAncestry: ancestors,
    oldParent,
  });

  // Give the docs new IDs without breaking internal references
  renewDocIds({
    docArray: nodes,
    collectionMap: { 'libraryNodes': 'creatureProperties' }
  });

  // Order the root node
  if (order === undefined) {
    setDocToLastOrder({
      collection: CreatureProperties,
      doc: node,
    });
  } else {
    node.order = order;
  }

  // Mark all nodes as dirty
  dirtyNodes(nodes);

  // Insert the creature properties
  CreatureProperties.batchInsert(nodes);
  return node;
}

function storeLibraryNodeReferences(nodes) {
  nodes.forEach(node => {
    if (node.libraryNodeId) return;
    node.libraryNodeId = node._id;
  });
}

function dirtyNodes(nodes) {
  nodes.forEach(node => {
    node.dirty = true;
  });
}

// Covert node references into actual nodes
// TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
  depth += 1;
  // New nodes added this function
  let newNodes = [];

  // Filter out the reference nodes we replace
  let resultingNodes = nodes.filter(node => {
    // This isn't a reference node, continue as normal
    if (node.type !== 'reference') return true;

    // We have gone too deep, keep the reference node as an error
    if (depth >= 10) {
      if (Meteor.isClient) console.warn('Reference depth limit exceeded');
      node.cache = { error: 'Reference depth limit exceeded' };
      return true;
    }

    let referencedNode
    try {
      referencedNode = fetchDocByRef(node.ref);
      referencedNode.order = node.order;
      referencedNode.tags = union(node.tags, referencedNode.tags);
      // We are definitely replacing this node, so add it to the list
      visitedRefs.add(node._id);
    } catch (e) {
      node.cache = { error: e.reason || e.message || e.toString() };
      return true;
    }

    // Get all the descendants of the referenced node
    let descendents = LibraryNodes.find({
      'ancestors.id': referencedNode._id,
      removed: { $ne: true },
    }, {
      sort: { order: 1 },
    }).fetch();

    // We are adding the referenced node and its descendants
    let addedNodes = [referencedNode, ...descendents];

    // re-map all the ancestors to parent the new sub-tree into our existing
    // node tree
    setLineageOfDocs({
      docArray: addedNodes,
      newAncestry: node.ancestors,
      oldParent: referencedNode.parent,
    });

    // Filter all the looped references
    addedNodes = addedNodes.filter(addedNode => {
      // Add all non-reference nodes
      if (addedNode.type !== 'reference') {
        return true;
      }
      // If this exact reference has already been resolved before, filter it out
      if (visitedRefs.has(addedNode._id)) {
        return false;
      } else {
        // Otherwise mark it as visited, and keep it
        visitedRefs.add(addedNode._id);
        return true;
      }
    });

    // Before renewing Ids make sure the library node reference is stored
    storeLibraryNodeReferences(addedNodes);

    // Give the new referenced sub-tree new ids
    // The referenced node must get the id of the ref node so that the
    // descendants of the ref node keep their ancestry intact
    renewDocIds({
      docArray: addedNodes,
      idMap: { [referencedNode._id]: node._id },
    });

    // Reify the subtree as well with recursion
    addedNodes = reifyNodeReferences(addedNodes, visitedRefs, depth);

    // Store the new nodes from this inner loop without altering the array
    // we are looping over
    newNodes.push(...addedNodes);
  });

  // We are done filtering the array, we can add the new nodes to it
  resultingNodes.push(...newNodes);

  return resultingNodes;
}

export default insertPropertyFromLibraryNode;
