<commit_before>import { Connectors, debug, debugGroup, debugGroupEnd /* runCallbacksAsync, runCallbacks, addCallback */ } from 'meteor/vulcan:core';
import { createError } from 'apollo-errors';
import Votes from './votes/collection.js';
import Users from 'meteor/vulcan:users';
import { recalculateScore } from './scoring.js';

/*

Define voting operations

*/
const voteTypes = {}

/*

Add new vote types

*/
export const addVoteType = (voteType, voteTypeOptions) => {
  voteTypes[voteType] = voteTypeOptions;
}

addVoteType('upvote', {power: 1, exclusive: true});
addVoteType('downvote', {power: -1, exclusive: true});

/*

Test if a user has voted on the client

*/
export const hasVotedClient = ({ document, voteType }) => {
  const userVotes = document.currentUserVotes;
  if (voteType) {
    return _.where(userVotes, { voteType }).length
  } else {
    return userVotes && userVotes.length
  }
}

/*

Calculate total power of all a user's votes on a document

*/
const calculateTotalPower = votes => _.pluck(votes, 'power').reduce((a, b) => a + b, 0);

/*

Test if a user has voted on the server

*/
const hasVotedServer = async ({ document, voteType, user }) => {
  const vote = await Connectors.get(Votes, {documentId: document._id, userId: user._id, voteType});
  return vote;
}

/*

Add a vote of a specific type on the client

*/
const addVoteClient = ({ document, collection, voteType, user, voteId }) => {

  const newDocument = {
    ...document,
    baseScore: document.baseScore || 0,
    __typename: collection.options.typeName,
    currentUserVotes: document.currentUserVotes || [],
  };

  // create new vote and add it to currentUserVotes array
  const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
  newDocument.currentUserVotes = [...newDocument.currentUserVotes, vote];

  // increment baseScore
  newDocument.baseScore += vote.power;
  newDocument.score = recalculateScore(newDocument);

  return newDocument;
}

/*

Add a vote of a specific type on the server

*/
const addVoteServer = async (voteOptions) => {

  const { document, collection, voteType, user, voteId, updateDocument } = voteOptions;
  const newDocument = _.clone(document);

  // create vote and insert it
  const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
  delete vote.__typename;
  await Connectors.create(Votes, vote);

  // initialize baseScore to vote power if not defined yet
  newDocument.baseScore = document.baseScore ? document.baseScore + vote.power : vote.power;
  newDocument.score = recalculateScore(newDocument);

  if (updateDocument) {
    // update document score & set item as active
    await Connectors.update(collection, {_id: document._id}, {$set: {inactive: false, baseScore: newDocument.baseScore, score: newDocument.score}});
  }

  return newDocument;
}

/*

Cancel votes of a specific type on a given document (client)

*/
const cancelVoteClient = ({ document, voteType }) => {
  const vote = _.findWhere(document.currentUserVotes, { voteType });
  const newDocument = _.clone(document);
  if (vote) {
    // subtract vote scores
    newDocument.baseScore -= vote.power;
    newDocument.score = recalculateScore(newDocument);

    const newVotes = _.reject(document.currentUserVotes, vote => vote.voteType === voteType);

    // clear out vote of this type
    newDocument.currentUserVotes = newVotes;

  }
  return newDocument;
}

/*

Clear *all* votes for a given document and user (client)

*/
const clearVotesClient = ({ document }) => {
  const newDocument = _.clone(document);
  newDocument.baseScore -= calculateTotalPower(document.currentUserVotes);
  newDocument.score = recalculateScore(newDocument);
  newDocument.currentUserVotes = [];
  return newDocument
}

/*

Clear all votes for a given document and user (server)

*/
const clearVotesServer = async ({ document, user, collection, updateDocument }) => {
  const newDocument = _.clone(document);
  const votes = await Connectors.find(Votes, { documentId: document._id, userId: user._id});
  if (votes.length) {
    await Connectors.delete(Votes, {documentId: document._id, userId: user._id});
    if (updateDocument) {
      await Connectors.update(collection, {_id: document._id}, {$inc: {baseScore: -calculateTotalPower(votes) }});
    }
    newDocument.baseScore -= calculateTotalPower(votes);
    newDocument.score = recalculateScore(newDocument);
  }
  return newDocument;
}

/*

Cancel votes of a specific type on a given document (server)

*/
const cancelVoteServer = async (existingVote, { document, voteType, collection, user, updateDocument }) => {

  const newDocument = _.clone(document);

  const vote = existingVote;

  // remove vote object
  await Connectors.delete(Votes, {_id: vote._id});

  if (updateDocument) {
    // update document score
    await Connectors.update(collection, {_id: document._id}, {$inc: {baseScore: -vote.power }});
  }

  newDocument.baseScore -= vote.power;
  newDocument.score = recalculateScore(newDocument);

  return newDocument;
}

/*

Determine a user's voting power for a given operation.
If power is a function, call it on user

*/
const getVotePower = ({ user, voteType, document }) => {
  const power = voteTypes[voteType] && voteTypes[voteType].power || 1;
  return typeof power === 'function' ? power(user, document) : power;
};

/*

Create new vote object

*/
const createVote = ({ document, collectionName, voteType, user, voteId }) => {

  const vote = {
    documentId: document._id,
    collectionName,
    userId: user._id,
    voteType: voteType,
    power: getVotePower({user, voteType, document}),
    votedAt: new Date(),
    __typename: 'Vote'
  }

  // when creating a vote from the server, voteId can sometimes be undefined
  if (voteId) vote._id = voteId;

  return vote;

};

/*

Optimistic response for votes

*/
export const performVoteClient = ({ document, collection, voteType = 'upvote', user, voteId }) => {

  const collectionName = collection.options.collectionName;
  let returnedDocument;

  // console.log('// voteOptimisticResponse')
  // console.log('collectionName: ', collectionName)
  // console.log('document:', document)
  // console.log('voteType:', voteType)

  // make sure item and user are defined
  if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {
    throw new Error(`Cannot perform operation '${collectionName.toLowerCase()}.${voteType}'`);
  }

  const voteOptions = {document, collection, voteType, user, voteId};

  if (hasVotedClient({document, voteType})) {

    // console.log('action: cancel')
    returnedDocument = cancelVoteClient(voteOptions);
    // returnedDocument = runCallbacks(`votes.cancel.client`, returnedDocument, collection, user);

  } else {

    // console.log('action: vote')

    if (voteTypes[voteType].exclusive) {
      clearVotesClient({document, collection, voteType, user, voteId})
    }

    returnedDocument = addVoteClient(voteOptions);
    // returnedDocument = runCallbacks(`votes.${voteType}.client`, returnedDocument, collection, user);

  }

  // console.log('returnedDocument:', returnedDocument)

  return returnedDocument;
}

/*

Server-side database operation

### updateDocument 
if set to true, this will perform its own database updates. If false, will only
return an updated document without performing any database operations on it. 

*/
export const performVoteServer = async ({ documentId, document, voteType = 'upvote', collection, voteId, user, updateDocument = true }) => {

  const collectionName = collection.options.collectionName;
  document = document || await Connectors.get(collection, documentId);

  debug('');<commit_msg>Updated voteOptions with document when clearVotes is called (PR worthy)<commit_after>import { Connectors, debug, debugGroup, debugGroupEnd /* runCallbacksAsync, runCallbacks, addCallback */ } from 'meteor/vulcan:core';
import { createError } from 'apollo-errors';
import Votes from './votes/collection.js';
import Users from 'meteor/vulcan:users';
import { recalculateScore } from './scoring.js';

/*

Define voting operations

*/
const voteTypes = {}

/*

Add new vote types

*/
export const addVoteType = (voteType, voteTypeOptions) => {
  voteTypes[voteType] = voteTypeOptions;
}

addVoteType('upvote', {power: 1, exclusive: true});
addVoteType('downvote', {power: -1, exclusive: true});

/*

Test if a user has voted on the client

*/
export const hasVotedClient = ({ document, voteType }) => {
  const userVotes = document.currentUserVotes;
  if (voteType) {
    return _.where(userVotes, { voteType }).length
  } else {
    return userVotes && userVotes.length
  }
}

/*

Calculate total power of all a user's votes on a document

*/
const calculateTotalPower = votes => _.pluck(votes, 'power').reduce((a, b) => a + b, 0);

/*

Test if a user has voted on the server

*/
const hasVotedServer = async ({ document, voteType, user }) => {
  const vote = await Connectors.get(Votes, {documentId: document._id, userId: user._id, voteType});
  return vote;
}

/*

Add a vote of a specific type on the client

*/
const addVoteClient = ({ document, collection, voteType, user, voteId }) => {

  const newDocument = {
    ...document,
    baseScore: document.baseScore || 0,
    __typename: collection.options.typeName,
    currentUserVotes: document.currentUserVotes || [],
  };

  // create new vote and add it to currentUserVotes array
  const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
  newDocument.currentUserVotes = [...newDocument.currentUserVotes, vote];

  // increment baseScore
  newDocument.baseScore += vote.power;
  newDocument.score = recalculateScore(newDocument);

  return newDocument;
}

/*

Add a vote of a specific type on the server

*/
const addVoteServer = async (voteOptions) => {

  const { document, collection, voteType, user, voteId, updateDocument } = voteOptions;
  const newDocument = _.clone(document);

  // create vote and insert it
  const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
  delete vote.__typename;
  await Connectors.create(Votes, vote);

  // initialize baseScore to vote power if not defined yet
  newDocument.baseScore = document.baseScore ? document.baseScore + vote.power : vote.power;
  newDocument.score = recalculateScore(newDocument);

  if (updateDocument) {
    // update document score & set item as active
    await Connectors.update(collection, {_id: document._id}, {$set: {inactive: false, baseScore: newDocument.baseScore, score: newDocument.score}});
  }

  return newDocument;
}

/*

Cancel votes of a specific type on a given document (client)

*/
const cancelVoteClient = ({ document, voteType }) => {
  const vote = _.findWhere(document.currentUserVotes, { voteType });
  const newDocument = _.clone(document);
  if (vote) {
    // subtract vote scores
    newDocument.baseScore -= vote.power;
    newDocument.score = recalculateScore(newDocument);

    const newVotes = _.reject(document.currentUserVotes, vote => vote.voteType === voteType);

    // clear out vote of this type
    newDocument.currentUserVotes = newVotes;

  }
  return newDocument;
}

/*

Clear *all* votes for a given document and user (client)

*/
const clearVotesClient = ({ document }) => {
  const newDocument = _.clone(document);
  newDocument.baseScore -= calculateTotalPower(document.currentUserVotes);
  newDocument.score = recalculateScore(newDocument);
  newDocument.currentUserVotes = [];
  return newDocument
}

/*

Clear all votes for a given document and user (server)

*/
const clearVotesServer = async ({ document, user, collection, updateDocument }) => {
  const newDocument = _.clone(document);
  const votes = await Connectors.find(Votes, { documentId: document._id, userId: user._id});
  if (votes.length) {
    await Connectors.delete(Votes, {documentId: document._id, userId: user._id});
    if (updateDocument) {
      await Connectors.update(collection, {_id: document._id}, {$inc: {baseScore: -calculateTotalPower(votes) }});
    }
    newDocument.baseScore -= calculateTotalPower(votes);
    newDocument.score = recalculateScore(newDocument);
  }
  return newDocument;
}

/*

Cancel votes of a specific type on a given document (server)

*/
const cancelVoteServer = async (existingVote, { document, voteType, collection, user, updateDocument }) => {

  const newDocument = _.clone(document);

  const vote = existingVote;

  // remove vote object
  await Connectors.delete(Votes, {_id: vote._id});

  if (updateDocument) {
    // update document score
    await Connectors.update(collection, {_id: document._id}, {$inc: {baseScore: -vote.power }});
  }

  newDocument.baseScore -= vote.power;
  newDocument.score = recalculateScore(newDocument);

  return newDocument;
}

/*

Determine a user's voting power for a given operation.
If power is a function, call it on user

*/
const getVotePower = ({ user, voteType, document }) => {
  const power = voteTypes[voteType] && voteTypes[voteType].power || 1;
  return typeof power === 'function' ? power(user, document) : power;
};

/*

Create new vote object

*/
const createVote = ({ document, collectionName, voteType, user, voteId }) => {

  const vote = {
    documentId: document._id,
    collectionName,
    userId: user._id,
    voteType: voteType,
    power: getVotePower({user, voteType, document}),
    votedAt: new Date(),
    __typename: 'Vote'
  }

  // when creating a vote from the server, voteId can sometimes be undefined
  if (voteId) vote._id = voteId;

  return vote;

};

/*

Optimistic response for votes

*/
export const performVoteClient = ({ document, collection, voteType = 'upvote', user, voteId }) => {

  const collectionName = collection.options.collectionName;
  let returnedDocument;

  // console.log('// voteOptimisticResponse')
  // console.log('collectionName: ', collectionName)
  // console.log('document:', document)
  // console.log('voteType:', voteType)

  // make sure item and user are defined
  if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {
    throw new Error(`Cannot perform operation '${collectionName.toLowerCase()}.${voteType}'`);
  }

  let voteOptions = {document, collection, voteType, user, voteId};

  if (hasVotedClient({document, voteType})) {

    // console.log('action: cancel')
    returnedDocument = cancelVoteClient(voteOptions);
    // returnedDocument = runCallbacks(`votes.cancel.client`, returnedDocument, collection, user);

  } else {

    // console.log('action: vote')

    if (voteTypes[voteType].exclusive) {
      voteOptions.document = clearVotesClient({document, collection, voteType, user, voteId})
    }

    returnedDocument = addVoteClient(voteOptions);
    // returnedDocument = runCallbacks(`votes.${voteType}.client`, returnedDocument, collection, user);

  }

  // console.log('returnedDocument:', returnedDocument)

  return returnedDocument;
}

/*

Server-side database operation

### updateDocument 
if set to true, this will perform its own database updates. If false, will only
return an updated document without performing any database operations on it. 

*/
export const performVoteServer = async ({ documentId, document, voteType = 'upvote', collection, voteId, user, updateDocument = true }) => {

  const collectionName = collection.options.collectionName;
  document = document || await Connectors.get(collection, documentId);

  debug('');
{'fraction_non_alphanumeric': 0.12438162544169612, 'fraction_numerical': 0.00064246707356248, 'mean_word_length': 4.204279505182213, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7047668', 'n_tokens_mistral': 4610, 'n_tokens_neox': 4540, 'n_words': 1315}