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 recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.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 recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';

const insertPropertyFromLibraryNode = new ValidatedMethod({
	name: 'creatureProperties.insertPropertyFromLibraryNode',
	validate: new SimpleSchema({
		nodeId: {
			type: String,
			regEx: SimpleSchema.RegEx.Id,
		},
		parentRef: {
			type: RefSchema,
		},
    order: {
      type: Number,
      optional: true,
    },
	}).validator(),
  mixins: [RateLimiterMixin],
  rateLimit: {
    numRequests: 5,
    timeInterval: 5000,
  },
	run({nodeId, 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);

		// 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) throw `Node not found for nodeId: ${nodeId}`;
		let oldParent = node.parent;
		let nodes = LibraryNodes.find({
			'ancestors.id': nodeId,
			removed: {$ne: true},
		}).fetch();

    // Convert all references into actual nodes
    nodes = reifyNodeReferences(nodes);

    // The root node is first in the array of nodes
    // It must get the first generated ID to prevent flickering
		nodes = [node, ...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;
    }

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

    // get the root inserted doc
    let rootId = node._id;

    // Tree structure changed by inserts, reorder the tree
    reorderDocs({
      collection: CreatureProperties,
      ancestorId: rootCreature._id,
    });

    // The library properties need to denormalise which of them are inactive
    recomputeInactiveProperties(rootId);
    // Some of the library properties may be items or containers
    recomputeInventory(rootCreature._id);
		// Inserting a creature property invalidates dependencies: full recompute
    recomputeCreatureByDoc(rootCreature);
		// Return the docId of the last property, the inserted root property
		return rootId;
	},
});

// 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 => {

    // We have already visited this ref and replaced it
    if (visitedRefs.has(node._id)) return false;

    // Already replaced an ancestor node
    for (let i; i < node.ancestors.length; i++){
      if (visitedRefs.has(node.ancestors[i].id)) return false;
    }

    // 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;
      // 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,
		});

    // Remove all the looped references and descendents from the new nodes
    // We can't rely on the reify recursion to do this, since the IDs are
    // getting renewed before it is called
    addedNodes = addedNodes.filter(node => {
      // Exclude removed referenced
      if (visitedRefs.has(node._id)) return false;

      // Exclude descendants of removed references
      for (let i; i < node.ancestors.length; i++){
        if (visitedRefs.has(node.ancestors[i].id)) return false;
      }
      return true;
    });

    // TODO: Force the referencedNode to take the old id of the reference
    // such that the reference's children can be kept
    
    // Give the new referenced sub-tree new ids
    renewDocIds({
      docArray: addedNodes,
    });

    // 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;
