<commit_before>import {Injectable} from '@angular/core';
import {NgRedux, dispatch} from '@angular-redux/store';
import {AnyAction} from 'redux';
import {List, Map, Set, isCollection, isImmutable} from 'immutable';

import {environment} from 'environments/environment';

import {
  CohortSearchState,
  activeItemId,
  activeGroupId,
  activeRole,
  isCriteriaLoading,
  isRequesting,
  getItem,
  getGroup,
  getSearchRequest,
  SR_ID,
} from '../store';
import * as ActionFuncs from './creators';

import {
  Criteria,
  CohortBuilderService,
  SearchRequest,
  SearchParameter,
  SearchGroup,
  SearchGroupItem,
} from 'generated';


@Injectable()
export class CohortSearchActions {
  constructor(private ngRedux: NgRedux<CohortSearchState>,
              private service: CohortBuilderService) {}

  /*
   * Auto-dispatched action creators:
   * We wrap the bare action creators in dispatch here so that we
   * (A) provide a unified action dispatching interface to components and
   * (B) can easily perform multi-step, complex actions from this service
   */
  @dispatch() requestCriteria = ActionFuncs.requestCriteria;
  @dispatch() cancelCriteriaRequest = ActionFuncs.cancelCriteriaRequest;

  @dispatch() requestCounts = ActionFuncs.requestCounts;
  @dispatch() cancelCountRequest = ActionFuncs.cancelCountRequest;

  @dispatch() _initGroup = ActionFuncs.initGroup;
  @dispatch() _initGroupItem = ActionFuncs.initGroupItem;
  @dispatch() selectCriteria = ActionFuncs.selectCriteria;
  @dispatch() _removeGroup = ActionFuncs.removeGroup;
  @dispatch() _removeGroupItem = ActionFuncs.removeGroupItem;
  @dispatch() _removeCriterion = ActionFuncs.removeCriterion;

  @dispatch() setWizardOpen = ActionFuncs.setWizardOpen;
  @dispatch() setWizardClosed = ActionFuncs.setWizardClosed;
  @dispatch() setActiveContext = ActionFuncs.setActiveContext;
  @dispatch() clearActiveContext = ActionFuncs.clearActiveContext;

  /** Internal tooling */
  _idsInUse = Set<string>();

  _generateId(prefix?: string) {
    prefix = prefix || 'id';
    let newId = `${prefix}${this._genSuffix()}`;
    while (this._idsInUse.has(newId)) {
      newId = `${prefix}${this._genSuffix()}`;
    }
    this._idsInUse = this._idsInUse.add(newId);
    return newId;
  }

  _genSuffix() {
    return Math.random().toString(36).substr(2, 9);
  }

  _removeId(id: string) {
    this._idsInUse = this._idsInUse.delete(id);
  }

  get state() {
    return this.ngRedux.getState();
  }

  debugDir(obj) {if (environment.debug) { console.dir(obj); }}
  debugLog(msg) {if (environment.debug) { console.log(msg); }}

  /* Higher order actions - actions composed of other actions or providing
   * alternate interfaces for a simpler action.
   */
  initGroup(role: keyof SearchRequest) {
    const newId = this._generateId(role);
    this._initGroup(role, newId);
  }

  initGroupItem(groupId: string) {
    const itemId = this._generateId('item');
    this._initGroupItem(itemId, groupId);
    this.setActiveContext({itemId});
  }

  cancelIfRequesting(kind, id) {
    if (isRequesting(kind, id)(this.state)) {
      this.cancelCountRequest(kind, id);
    }
  }

  removeGroup(role: keyof SearchRequest, groupId: string): void {
    const group = getGroup(groupId)(this.state);
    this.debugLog(`Removing ${groupId} of ${role}:`);
    this.debugDir(group);

    this.cancelIfRequesting('groups', groupId);

    this._removeGroup(role, groupId);
    this._removeId(groupId);

    group.get('items', List()).forEach(itemId => {
      this.cancelIfRequesting('items', itemId);
      this._removeGroupItem(groupId, itemId);
      this._removeId(itemId);
    });

    const hasItems = !group.get('items', List()).isEmpty();
    if (hasItems) {
      this.requestTotalCount();
    }
  }

  removeGroupItem(role: keyof SearchRequest, groupId: string, itemId: string): void {
    const item = getItem(itemId)(this.state);
    const hasItems = !item.get('searchParameters', List()).isEmpty();
    const countIsNonZero = item.get('count') !== 0;
    // The optimization wherein we only fire the request if the item
    // has a non-zero count (i.e. it affects its group and hence the total
    // counts) ONLY WORKS if the item is NOT an only child.
    const isOnlyChild = (getGroup(groupId)(this.state))
      .get('items', List())
      .equals(List([itemId]));

    this.debugLog(`Removing ${itemId} of ${groupId}:`);
    this.debugDir(item);
    this.debugLog(
      `hasItems: ${hasItems}, countIsNonZero: ${countIsNonZero}, isOnlyChild: ${isOnlyChild}`
    );

    this.cancelIfRequesting('items', itemId);
    this._removeGroupItem(groupId, itemId);
    this._removeId(itemId);

    item.get('searchParameters', List()).forEach(id => {
      this._removeCriterion(itemId, id);
    });

    if (hasItems && (countIsNonZero || isOnlyChild)) {
      this.requestGroupCount(role, groupId);
      this.requestTotalCount();
    }
  }

  removeCriterion(
    role: keyof SearchRequest, groupId: string, itemId: string, criterionId: number
  ): void {
    /* If we're not in a wizard context, then we need to update all the counts.
     * Otherwise, finishing the wizard will effectively do that for us
     */
    if (!this.state.getIn(['context', 'wizardOpen'], false)) {
      this.requestGroupCount(role, groupId);
      this.requestItemCount(role, itemId);
      this.requestTotalCount();
    }
    this._removeCriterion(itemId, criterionId);
  }

  openWizard(criteriaType: string, role: keyof SearchRequest, groupId: string): void {
    this.setActiveContext({criteriaType, role, groupId});
    this.setWizardOpen();
    this.initGroupItem(groupId);
  }

  finishWizard(): void {
    this.requestItemCount(activeRole(this.state), activeItemId(this.state));
    this.requestGroupCount(activeRole(this.state), activeGroupId(this.state));
    this.requestTotalCount();

    this.clearActiveContext();
    this.setWizardClosed();
  }

  cancelWizard(): void {
    this.setWizardClosed();
    this.removeGroupItem(
      activeRole(this.state),
      activeGroupId(this.state),
      activeItemId(this.state)
    );
    this.clearActiveContext();
  }

  fetchCriteria(kind: string, parentId: number): void {
    const isLoading = isCriteriaLoading(kind, parentId)(this.state);
    const isLoaded = this.state.getIn(['criteria', kind, parentId]);
    if (isLoaded || isLoading) {
      return;
    }
    this.requestCriteria(kind, parentId);
  }

  requestItemCount(role: keyof SearchRequest, itemId: string): void {
    const item = getItem(itemId)(this.state);
    if (item.get('isRequesting', false)) {
      this.cancelCountRequest('items', itemId);
    }
    const request = <SearchRequest>{
      includes: [],
      excludes: [],
      [role]: [{
        items: [this.mapGroupItem(itemId)],
      }]
    };
    this.requestCounts('items', itemId, request);
  }

  requestGroupCount(role: keyof SearchRequest, groupId: string): void {
    const group = getGroup(groupId)(this.state);
    if (group.get('isRequesting', false)) {
      this.cancelCountRequest('groups', groupId);
    }
    const request = <SearchRequest>{
      includes: [],
      excludes: [],
      [role]: [this.mapGroup(groupId)]
    };
    this.requestCounts('groups', groupId, request);
  }

  requestTotalCount(): void {
    const searchRequest = getSearchRequest(SR_ID)(this.state);
    if (searchRequest.get('isRequesting', false)) {
      this.cancelCountRequest('searchRequests', SR_ID);
    }
    const request = this.mapAll();
    // TODO(jms) if the request is completely empty, the total count will be
    // zero: just set it to zero
    this.requestCounts('searchRequests', SR_ID, request);
  }

  mapAll = (): SearchRequest => {<commit_msg>Set count directly to zero for some request situations (#226)<commit_after>import {Injectable} from '@angular/core';
import {NgRedux, dispatch} from '@angular-redux/store';
import {AnyAction} from 'redux';
import {List, Map, Set, isCollection, isImmutable} from 'immutable';

import {environment} from 'environments/environment';

import {
  CohortSearchState,
  activeItemId,
  activeGroupId,
  activeRole,
  isCriteriaLoading,
  isRequesting,
  includeGroups,
  getItem,
  getGroup,
  getSearchRequest,
  SR_ID,
} from '../store';
import * as ActionFuncs from './creators';

import {
  Criteria,
  CohortBuilderService,
  SearchRequest,
  SearchParameter,
  SearchGroup,
  SearchGroupItem,
} from 'generated';


@Injectable()
export class CohortSearchActions {
  constructor(private ngRedux: NgRedux<CohortSearchState>,
              private service: CohortBuilderService) {}

  /*
   * Auto-dispatched action creators:
   * We wrap the bare action creators in dispatch here so that we
   * (A) provide a unified action dispatching interface to components and
   * (B) can easily perform multi-step, complex actions from this service
   */
  @dispatch() requestCriteria = ActionFuncs.requestCriteria;
  @dispatch() cancelCriteriaRequest = ActionFuncs.cancelCriteriaRequest;

  @dispatch() requestCounts = ActionFuncs.requestCounts;
  @dispatch() cancelCountRequest = ActionFuncs.cancelCountRequest;
  @dispatch() setCount = ActionFuncs.loadCountRequestResults;

  @dispatch() _initGroup = ActionFuncs.initGroup;
  @dispatch() _initGroupItem = ActionFuncs.initGroupItem;
  @dispatch() selectCriteria = ActionFuncs.selectCriteria;
  @dispatch() _removeGroup = ActionFuncs.removeGroup;
  @dispatch() _removeGroupItem = ActionFuncs.removeGroupItem;
  @dispatch() _removeCriterion = ActionFuncs.removeCriterion;

  @dispatch() setWizardOpen = ActionFuncs.setWizardOpen;
  @dispatch() setWizardClosed = ActionFuncs.setWizardClosed;
  @dispatch() setActiveContext = ActionFuncs.setActiveContext;
  @dispatch() clearActiveContext = ActionFuncs.clearActiveContext;

  /** Internal tooling */
  _idsInUse = Set<string>();

  _generateId(prefix?: string) {
    prefix = prefix || 'id';
    let newId = `${prefix}${this._genSuffix()}`;
    while (this._idsInUse.has(newId)) {
      newId = `${prefix}${this._genSuffix()}`;
    }
    this._idsInUse = this._idsInUse.add(newId);
    return newId;
  }

  _genSuffix() {
    return Math.random().toString(36).substr(2, 9);
  }

  _removeId(id: string) {
    this._idsInUse = this._idsInUse.delete(id);
  }

  get state() {
    return this.ngRedux.getState();
  }

  debugDir(obj) {if (environment.debug) { console.dir(obj); }}
  debugLog(msg) {if (environment.debug) { console.log(msg); }}

  /* Higher order actions - actions composed of other actions or providing
   * alternate interfaces for a simpler action.
   */
  initGroup(role: keyof SearchRequest) {
    const newId = this._generateId(role);
    this._initGroup(role, newId);
  }

  initGroupItem(groupId: string) {
    const itemId = this._generateId('item');
    this._initGroupItem(itemId, groupId);
    this.setActiveContext({itemId});
  }

  cancelIfRequesting(kind, id) {
    if (isRequesting(kind, id)(this.state)) {
      this.cancelCountRequest(kind, id);
    }
  }

  removeGroup(role: keyof SearchRequest, groupId: string): void {
    const group = getGroup(groupId)(this.state);
    this.debugLog(`Removing ${groupId} of ${role}:`);
    this.debugDir(group);

    this.cancelIfRequesting('groups', groupId);

    this._removeGroup(role, groupId);
    this._removeId(groupId);

    group.get('items', List()).forEach(itemId => {
      this.cancelIfRequesting('items', itemId);
      this._removeGroupItem(groupId, itemId);
      this._removeId(itemId);
    });

    const hasItems = !group.get('items', List()).isEmpty();
    if (hasItems) {
      this.requestTotalCount();
    }
  }

  removeGroupItem(role: keyof SearchRequest, groupId: string, itemId: string): void {
    const item = getItem(itemId)(this.state);
    const hasItems = !item.get('searchParameters', List()).isEmpty();
    const countIsNonZero = item.get('count') !== 0;
    // The optimization wherein we only fire the request if the item
    // has a non-zero count (i.e. it affects its group and hence the total
    // counts) ONLY WORKS if the item is NOT an only child.
    const isOnlyChild = (getGroup(groupId)(this.state))
      .get('items', List())
      .equals(List([itemId]));

    this.debugLog(`Removing ${itemId} of ${groupId}:`);
    this.debugDir(item);
    this.debugLog(
      `hasItems: ${hasItems}, countIsNonZero: ${countIsNonZero}, isOnlyChild: ${isOnlyChild}`
    );

    this.cancelIfRequesting('items', itemId);
    this._removeGroupItem(groupId, itemId);
    this._removeId(itemId);

    item.get('searchParameters', List()).forEach(id => {
      this._removeCriterion(itemId, id);
    });

    if (hasItems && (countIsNonZero || isOnlyChild)) {
      this.requestTotalCount();

      /* If this was the only item in the group, the group no longer has a
       * count, not really. */
      if (isOnlyChild) {
        this.setCount('groups', groupId, null);
      } else {
        this.requestGroupCount(role, groupId);
      }
    }
  }

  removeCriterion(
    role: keyof SearchRequest, groupId: string, itemId: string, criterionId: number
  ): void {
    /* If we're not in a wizard context, then we need to update all the counts.
     * Otherwise, finishing the wizard will effectively do that for us
     */
    if (!this.state.getIn(['context', 'wizardOpen'], false)) {
      this.requestGroupCount(role, groupId);
      this.requestItemCount(role, itemId);
      this.requestTotalCount();
    }
    this._removeCriterion(itemId, criterionId);
  }

  openWizard(criteriaType: string, role: keyof SearchRequest, groupId: string): void {
    this.setActiveContext({criteriaType, role, groupId});
    this.setWizardOpen();
    this.initGroupItem(groupId);
  }

  finishWizard(): void {
    this.requestItemCount(activeRole(this.state), activeItemId(this.state));
    this.requestGroupCount(activeRole(this.state), activeGroupId(this.state));
    this.requestTotalCount();

    this.clearActiveContext();
    this.setWizardClosed();
  }

  cancelWizard(): void {
    this.setWizardClosed();
    this.removeGroupItem(
      activeRole(this.state),
      activeGroupId(this.state),
      activeItemId(this.state)
    );
    this.clearActiveContext();
  }

  fetchCriteria(kind: string, parentId: number): void {
    const isLoading = isCriteriaLoading(kind, parentId)(this.state);
    const isLoaded = this.state.getIn(['criteria', kind, parentId]);
    if (isLoaded || isLoading) {
      return;
    }
    this.requestCriteria(kind, parentId);
  }

  requestItemCount(role: keyof SearchRequest, itemId: string): void {
    const item = getItem(itemId)(this.state);
    if (item.get('isRequesting', false)) {
      this.cancelCountRequest('items', itemId);
    }
    const request = <SearchRequest>{
      includes: [],
      excludes: [],
      [role]: [{
        items: [this.mapGroupItem(itemId)],
      }]
    };
    this.requestCounts('items', itemId, request);
  }

  requestGroupCount(role: keyof SearchRequest, groupId: string): void {
    const group = getGroup(groupId)(this.state);
    if (group.get('isRequesting', false)) {
      this.cancelCountRequest('groups', groupId);
    }
    const request = <SearchRequest>{
      includes: [],
      excludes: [],
      [role]: [this.mapGroup(groupId)]
    };
    this.requestCounts('groups', groupId, request);
  }

  requestTotalCount(): void {
    const searchRequest = getSearchRequest(SR_ID)(this.state);
    if (searchRequest.get('isRequesting', false)) {
      this.cancelCountRequest('searchRequests', SR_ID);
    }

    const included = includeGroups(this.state);
    /* If there are no members of an intersection, the intersection is the null
     * set */
    const emptyIntersection = included.size === 0;
    /* If any member of an intersection is the null set, the intersection is
     * the null set */
    const nullIntersection = included.some(group => group.get('count') === 0);
    /* In either case the total count is provably zero without needing to ask
     * the API */
    if (nullIntersection || emptyIntersection) {
      this.setCount('searchRequests', SR_ID, 0);
      return ;
    }

    const request = this.mapAll();
    this.requestCounts('searchRequests', SR_ID, request);
  }

  mapAll = (): SearchRequest => {
{'fraction_non_alphanumeric': 0.13199975304068653, 'fraction_numerical': 0.0009878372538124345, 'mean_word_length': 3.8179654967281382, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 18, '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': '2268228', 'n_tokens_mistral': 5020, 'n_tokens_neox': 4904, 'n_words': 1125}