<commit_before>moment = require 'moment-timezone'
twix = require 'twix'
_ = require 'underscore'
camelCase = require 'camelcase'

React = require 'react/addons'
CoursePlan = require './plan'
PlanHelper = require '../../helpers/plan'
TimeHelper = require '../../helpers/time'
{TimeStore} = require '../../flux/time'

CourseDuration = React.createClass
  displayName: 'CourseDuration'

  propTypes:
    durations: React.PropTypes.array.isRequired
    courseId: React.PropTypes.string.isRequired
    viewingDuration: React.PropTypes.instanceOf(twix).isRequired
    groupingDurations: React.PropTypes.arrayOf(React.PropTypes.instanceOf(twix)).isRequired
    referenceDate: TimeHelper.PropTypes.moment
    children: React.PropTypes.element

  getInitialState: ->
    ranges: []
    durationsByStartDate: []

  updateGroupedDurations: (props) ->
    {durations, viewingDuration, groupingDurations} = props

    groupedDurations = @groupDurations(durations, viewingDuration, groupingDurations)

    durationsByStartDate = _.chain(groupedDurations)
      .pluck('plansInRange')
      .flatten()
      .groupBy((rangedPlan) ->
        rangedPlan.plan.id
      )
      .map((groupedPlans) ->
        # Grab the first plan as representative plan for displays grouped by plan
        {plan} = _.first(groupedPlans)

        # omit the plan on the individual displays since they share one common plan
        # TODO: clean up so that plan doesn't get duplicated in the first place.
        displays = _.map(groupedPlans, (groupedPlan) ->
          _.omit(groupedPlan, 'plan')
        )

        {plan, displays}
      )
      .value()

    @setState({ranges: groupedDurations, durationsByStartDate})

  componentWillMount: ->
    @updateGroupedDurations(@props)

  componentWillReceiveProps: (nextProps) ->
    @updateGroupedDurations(nextProps)

  groupDurations: (durations, viewingDuration, groupingDurations) ->
    durationsInView = _.chain(durations)
      .clone()
      # TODO these parts actually seem like they should be in flux
      .each(@setDuration(viewingDuration))
      .filter(@isInDuration(viewingDuration))
      .sortBy((plan) ->
        moment(plan.published_at).valueOf()
      )
      .sortBy((plan) ->
        plan.duration.end().valueOf()
      )
      .sortBy((plan) ->
        plan.duration.start().valueOf()
      )
      .value()

    groupedDurations = _.chain(groupingDurations)
      .map(@groupByRanges(durationsInView))
      .each(@calcDurationHeight)
      .tap(@calcTopOffset)
      .value()

  calcDurationHeight: (rangeData) ->
    planGroups = _.reduce(rangeData.plansByDays, (groups, plansByDay) ->

      planIds = _.map(plansByDay, (planInfo) -> planInfo.plan.id)

      # if plans don't group together on adjacent days,
      if _.isEmpty(_.intersection(_.last(groups), planIds))
        # push as new group
        groups.push(planIds)
      else
        # else, plans share adjacent days, group together.
        groups[groups.length - 1] = _.union(_.last(groups), planIds)

      groups

    , [[]])

    rangeData.maxPlansOnDay = _.max(planGroups, (plansInGroup) ->
      plansInGroup.length
    ).length

    # set day height to the best-guess for this range based on how many plans it has.
    # It'll be fine-tuned later across all ranges
    rangeData.dayHeight = Math.max(
      @_calcDayHeight(rangeData.maxPlansOnDay), rangeData.dayHeight
    )

  calcTopOffset: (ranges) ->
    dayHeights = _.pluck(ranges, 'dayHeight')

    _.each(ranges, (range, index) =>
      weekTopOffset = _.chain(dayHeights).first(index + 1).reduce((memo, current) ->
        memo + current
      ).value()

      {maxPlansOnDay, plansByDays} = range
      _.each(plansByDays, (plans) =>
        current =
          adder: 0

        # grab all existing orders in the day
        existingOrdered = _.chain(plans)
          .pluck('order')
          .compact()
          .value()

        _.chain(plans)
          .sortBy((plan) ->
            -1 * plan.rangeDuration.start.valueOf()
          )
          .each(@setPlanOrder({current, existingOrdered, weekTopOffset, maxPlansOnDay}))
          .value()
      )
    )

  # set plan order, makes sure that order is not already taken on this day
  setPlanOrder: ({current, existingOrdered, weekTopOffset, maxPlansOnDay}) ->
    (duration, order) =>
      unless duration.order?
        current.order = order
        @_calcOrder({existingOrdered, current, maxPlansOnDay})
        duration.order = current.order
        duration.weekTopOffset = weekTopOffset

  _calcOrder: ({existingOrdered, current, maxPlansOnDay}) ->
    # find an order that is not already occupied by any overlapping plans

    while existingOrdered.indexOf(maxPlansOnDay - (current.order + current.adder)) > -1
      current.adder = current.adder + 1

    current.order = maxPlansOnDay - (current.order + current.adder)

  _getDay: (oneMoment) ->
    moment(oneMoment).startOf('day').twix(moment(oneMoment).endOf('day'), {allDay: true})

  _getDurationFromMoments: (listOfMoments) ->
    _.reduce listOfMoments, (current, next) =>
      nextDay = @_getDay(next)

      current.union(nextDay)
    , @_getDay(listOfMoments[0])


  _getDurationRange: (plan) ->
    openDates = _.pluck(plan.tasking_plans, 'opens_at')
    dueDates = _.pluck(plan.tasking_plans, 'due_at')
<commit_msg>take order of plan stack into account when determining the number of plans a week needs to accomodate<commit_after>moment = require 'moment-timezone'
twix = require 'twix'
_ = require 'underscore'
camelCase = require 'camelcase'

React = require 'react/addons'
CoursePlan = require './plan'
PlanHelper = require '../../helpers/plan'
TimeHelper = require '../../helpers/time'
{TimeStore} = require '../../flux/time'

CourseDuration = React.createClass
  displayName: 'CourseDuration'

  propTypes:
    durations: React.PropTypes.array.isRequired
    courseId: React.PropTypes.string.isRequired
    viewingDuration: React.PropTypes.instanceOf(twix).isRequired
    groupingDurations: React.PropTypes.arrayOf(React.PropTypes.instanceOf(twix)).isRequired
    referenceDate: TimeHelper.PropTypes.moment
    children: React.PropTypes.element

  getInitialState: ->
    ranges: []
    durationsByStartDate: []

  updateGroupedDurations: (props) ->
    {durations, viewingDuration, groupingDurations} = props

    groupedDurations = @groupDurations(durations, viewingDuration, groupingDurations)

    durationsByStartDate = _.chain(groupedDurations)
      .pluck('plansInRange')
      .flatten()
      .groupBy((rangedPlan) ->
        rangedPlan.plan.id
      )
      .map((groupedPlans) ->
        # Grab the first plan as representative plan for displays grouped by plan
        {plan} = _.first(groupedPlans)

        # omit the plan on the individual displays since they share one common plan
        # TODO: clean up so that plan doesn't get duplicated in the first place.
        displays = _.map(groupedPlans, (groupedPlan) ->
          _.omit(groupedPlan, 'plan')
        )

        {plan, displays}
      )
      .value()

    @setState({ranges: groupedDurations, durationsByStartDate})

  componentWillMount: ->
    @updateGroupedDurations(@props)

  componentWillReceiveProps: (nextProps) ->
    @updateGroupedDurations(nextProps)

  groupDurations: (durations, viewingDuration, groupingDurations) ->
    durationsInView = _.chain(durations)
      .clone()
      # TODO these parts actually seem like they should be in flux
      .each(@setDuration(viewingDuration))
      .filter(@isInDuration(viewingDuration))
      .sortBy((plan) ->
        moment(plan.published_at).valueOf()
      )
      .sortBy((plan) ->
        plan.duration.end().valueOf()
      )
      .sortBy((plan) ->
        plan.duration.start().valueOf()
      )
      .value()

    groupedDurations = _.chain(groupingDurations)
      .map(@groupByRanges(durationsInView))
      .each(@calcDurationHeight)
      .tap(@calcTopOffset)
      .value()

  calcDurationHeight: (rangeData) ->
    overlappingPlans = []
    planStacks = []

    _.each(rangeData.plansByDays, (plansByDay) =>
      @setPlanRelativeOrder(plansByDay)

      planIds = _.map(plansByDay, (planInfo) -> planInfo.plan.id)
      planOrder = _.map(plansByDay, (planInfo) -> planInfo.relativeOrder)

      # if plans don't group together on adjacent days,
      if _.isEmpty(_.intersection(_.last(overlappingPlans), planIds))
        # push as new group
        overlappingPlans.push(planIds)
        planStacks.push(planOrder)
      else
        # else, plans share adjacent days, group together.
        overlappingPlans[overlappingPlans.length - 1] = _.union(_.last(overlappingPlans), planIds)
        planStacks[planStacks.length - 1] = _.union(_.last(planStacks), planOrder)
    )

    rangeData.maxPlansOnDay = _.max(planStacks, (plansInStack) ->
      plansInStack.length
    ).length

    # set day height to the best-guess for this range based on how many plans it has.
    # It'll be fine-tuned later across all ranges
    rangeData.dayHeight = Math.max(
      @_calcDayHeight(rangeData.maxPlansOnDay), rangeData.dayHeight
    )

  calcTopOffset: (ranges) ->
    dayHeights = _.pluck(ranges, 'dayHeight')

    _.each(ranges, (range, index) =>
      weekTopOffset = _.chain(dayHeights).first(index + 1).reduce((memo, current) ->
        memo + current
      ).value()

      {maxPlansOnDay, plansByDays} = range
      _.each(plansByDays, (plans) ->
        _.each(plans, (duration) ->
          duration.weekTopOffset = weekTopOffset
          duration.order = maxPlansOnDay + duration.relativeOrder
        )
      )
    )

  setPlanRelativeOrder: (plans) ->
    current =
      adder: 0

    # grab all existing orders in the day
    existingOrdered = _.chain(plans).pluck('relativeOrder').compact().value()

    _.each(plans, @_setPlanOrder({current, existingOrdered}))

  # set plan order, makes sure that order is not already taken on this day
  _setPlanOrder: ({current, existingOrdered}) ->
    (duration, order) =>
      duration.relativeOrder ?= @_calcOrder({existingOrdered, current, order})

  _calcOrder: ({existingOrdered, current, order}) ->
    # find an order that is not already occupied by any overlapping plans
    while existingOrdered.indexOf(- (order + current.adder)) > -1
      current.adder = current.adder + 1

    - (order + current.adder)

  _getDay: (oneMoment) ->
    moment(oneMoment).startOf('day').twix(moment(oneMoment).endOf('day'), {allDay: true})

  _getDurationFromMoments: (listOfMoments) ->
    _.reduce listOfMoments, (current, next) =>
      nextDay = @_getDay(next)

      current.union(nextDay)
    , @_getDay(listOfMoments[0])


  _getDurationRange: (plan) ->
    openDates = _.pluck(plan.tasking_plans, 'opens_at')
    dueDates = _.pluck(plan.tasking_plans, 'due_at')

{'fraction_non_alphanumeric': 0.11758700696055685, 'fraction_numerical': 0.0012993039443155452, 'mean_word_length': 3.0618168111571804, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 61, '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': '366289', 'n_tokens_mistral': 3364, 'n_tokens_neox': 3250, 'n_words': 715}