<commit_before>#////////////////////////////////////////////////////////////
# Exploratory reimplementation of considerit client in React
#////////////////////////////////////////////////////////////

# Ugliness in this prototype: 
#   - Provision of @data() as a function via reactiveREST, rather than a straight up object. I couldn't
#     figure out a way to do it more cleanly. 
#   - Children needing to know the parent Proposal's key, and using active_proposal_key() to get it
#   - Programmer needing to know multiple ActiveREST cache keys to save new points to
#   - UI state and persisted data stored in same place (when saving, what to send to server?)
#   - Figuring out if the route was changed. Or more generally, knowing what the diff in state was
#     in the lifecycle methods now that we're not using @props or @state. Right now using a hack
#     (see use of _route_changed ActiveREST cache key)
#   - All UI state _must_ be declared in the @getDefaultData method for the component at which
#     that state will be stored. Otherwise, getters and setters won't be operative!
#   - Computed state not yet stored in ActiveREST
#   - Easy for programmer to accidently mess up ActiveREST cache
#   - Managing top_level_component in Router
#   - NewPoint CSS/HTML is still bulky, waiting on redesign

# ##
# React aliases
R = React.DOM

# ##
# Constants, especially used for layout styling
TRANSITION_SPEED = 700   # Speed of transition from results to crafting (and vice versa) 
BIGGEST_POSSIBLE_AVATAR_SIZE = 50
GRID_MAJOR = 48  #GRID_MAJOR = GRID_MINOR + GRID_GUTTER
GRID_MINOR = 30
GRID_GUTTER = 18
PAGE_WIDTH = 24 * GRID_MAJOR
CONTENT_WIDTH = PAGE_WIDTH - 4 * GRID_MAJOR
BODY_WIDTH = CONTENT_WIDTH - 8 * GRID_MAJOR - 2 * GRID_GUTTER
POINT_WIDTH = BODY_WIDTH / 2 - 20
POINT_CONTENT_WIDTH = POINT_WIDTH - 35 - GRID_GUTTER
HISTOGRAM_WIDTH = BODY_WIDTH    # Width of the slider / histogram base 
DECISION_BOARD_WIDTH = BODY_WIDTH + 4 # the four is for the border
REASONS_REGION_WIDTH = DECISION_BOARD_WIDTH + 2 * POINT_CONTENT_WIDTH + 4 * GRID_GUTTER + 4
MAX_HISTOGRAM_HEIGHT = 200


##
# Helpers that should probably go elsewhere

capitalize = (string) -> string.charAt(0).toUpperCase() + string.substring(1)

# Returns the top level Proposal component key. Components use this to grab data
# from its Proposal. This is a hack. Shadowing parent state would solve the problem.
active_proposal_key = -> 
  "proposal/#{ActiveREST.fetch('application').route.split('/')[1]}"

getStanceSegment = (value) ->
  for i in [0..5]
    if value < stanceSegmentBoundaries[i + 1]
      return i
  return 6

togglePage = (page) -> 
  $('.histogram_segment_pole').ensureInView
    offset_buffer: 150
    callback : -> 
      top_level_component.data('application').page = page


stanceSegmentBoundaries = { 0 : -1, 1 : -.9999, 2 : -0.5, 3 : -0.05, 4 : 0.05, 5 : 0.5, 6 : .9999 } 

stance_names = 
  6 : 'Entrenched Supporter'
  0 : 'Entrenched Opposer'
  5 : 'Firmly Support'
  1 : 'Firmly Oppose'
  4 : 'Lean Support'
  2 : 'Lean Oppose'
  3 : 'Neutral'

##
# StyleAnimator mixin
# Helper for components that implement animations.
# Applies styles defined in the component that change
# depending on state. Right now this mixin assumes that
# animations will be applied only on route changes. 
#
# A component that implements this mixin should define a method:
#     applyStyles : (animate = true)
# which puts the styles on the desired elements. 
StyleAnimator = 
  componentDidMount : -> @applyStyles false

  componentDidUpdate : (prev_props, prev_page) -> 
    @applyStyles ActiveREST.fetch('_route_changed')

  applyStylesToElements : (styles, duration) ->
    $el = $(@getDOMNode())
    _.each _.keys(styles), (selector) -> 
      # Only apply styles if there are differences, for performance. 
      # This check doesn't work for transform properties because for Velocity we 
      # specify them as e.g. TranslateX: 4 rather than transform: translateX(4)
      styles_for_selector = styles[selector]
      styles_to_apply = {}
      $target = $el.find(selector)

      prop_map = $target.css(_.keys(styles_for_selector))
      if prop_map
        for property in _.keys(styles_for_selector)
          if prop_map[property] != styles_for_selector[property]
            styles_to_apply[property] = styles_for_selector[property]

      if _.size(styles_to_apply) > 0
        $target.velocity styles_to_apply, {duration}


## ##################
# React Components
#
# These are the components and their relationships:
#
#                     Application 
#                          |
#                       Proposal
#                   /      |           \            \
#    CommunityPoints   DecisionBoard   Histogram    Slider
#               |          |
#               |      YourPoints
#               |    /            \
#              Point             NewPoint
#
#   (also, Proposal, Histogram, and Point employ Avatar)


##
# Proposal
# Has proposal description, feelings area (slider + histogram), and reasons area
Proposal = ReactiveComponent
  mixins: [StyleAnimator]
  displayName: 'Proposal'

  ##
  # Component defaults

  getDefaultData : ->
    proposal : {}
    points : {}
    users : {}
    included_points : []
    initial_stance : 0.0
    opinions : []
    # UI STATE
    highlighted_users : null
    sticky_opinion : false
    sticky_proposal_title: false

  ##
  # Lifecycle methods

  componentDidMount : (prev_props, prev_state) ->

  componentDidUpdate : ->
    @setStickyHeader()

    $el = $(@getDOMNode())
    $el.find('.description_region').css
      top: $el.find('.fixed_proposal_heading').height() + 24


  setStickyHeader : ->
    # Sticky decision board. 
    _.delay => 
      $el = $(@getDOMNode())
      $sticky_head_el = $el.find('.slider_base')
      $(@getDOMNode()).headroom
        offset: $sticky_head_el.offset().top - $el.find('.fixed_proposal_heading').outerHeight() - 55 #+ $sticky_head_el.width() / 2 - 60
        onNotTop : => @data().sticky_opinion = true
        onTop : => @data().sticky_opinion = false
    , 2000  # delay initialization to let the rest of the dom load 
            # so that the offset is calculated properly

    # # Sticky proposal title
    # _.delay => 
    #   $sticky_head_el = $(@getDOMNode()).find('.proposal_heading')
    #   $(@getDOMNode()).find('.description_region').headroom
    #     offset: $sticky_head_el.offset().top + 25
    #     onNotTop : => 
    #       @data().sticky_proposal_title = true
    #     onTop : => @data().sticky_proposal_title = false
    # , 2000  # delay initialization to let the rest of the dom load 
    #         # so that the offset is calculated properly

    

  ##
  # State-dependent styling
  applyStyles : (animate = true) ->  
    $el = $(@getDOMNode())
    duration = if animate then TRANSITION_SPEED else 0
    page = @data('application').page

    # • Velocity requires properties to be pulled out (e.g. paddingLeft, 
    #   translateX, rather than using padding or transform)
    # • We use velocity even for 0 duration style applications to maintain 
    #   parity of style definition
    switch page
      when 'crafting'
        styles = 
          '.histogram_bar':        { opacity: '.2' }
          '.opinion_region':       { translateX: 0, translateY: 0 }
          '.decision_board_body':  { width: "#{DECISION_BOARD_WIDTH}px", minHeight: "375px"}
          '.pros_by_community':    { translateX: 0 }
          '.cons_by_community':    { translateX: "#{DECISION_BOARD_WIDTH}px" }
        
        @applyStylesToElements styles, duration

        $el.find('.give_opinion_button').css 'visibility', 'hidden'
        _.delay => 
          #delay & check in case of quick switch between pages
          if @data('application').page == 'crafting'         
            $el.find('.your_points, .save_opinion_button, .cancel_opinion_button').css 'display', ''
        , duration

      when 'results'
        slider_stance = $el.find('.ui-slider').slider('value')
        segment = getStanceSegment(-1 * slider_stance)

        opinion_region_x = DECISION_BOARD_WIDTH * (slider_stance+1) / 2.0
        give_opinion_button_width = 186
        if segment == 3
          opinion_region_x -= give_opinion_button_width / 2
        else if segment < 3
          opinion_region_x -= give_opinion_button_width * .95 
        else 
          opinion_region_x -= give_opinion_button_width * .05 


        styles = 
          '.histogram_bar':         { opacity: '1' }
          '.opinion_region':        { translateX: opinion_region_x, translateY: -18 }
          '.decision_board_body':   { width: "#{give_opinion_button_width}px", minHeight: "32px"}
          '.pros_by_community':     { translateX:  DECISION_BOARD_WIDTH / 2 }
          '.cons_by_community':     { translateX:  DECISION_BOARD_WIDTH / 2 }
        
        @applyStylesToElements styles, duration

        $el.find('.your_points, .save_opinion_button, .cancel_opinion_button').css 'display', 'none'
        _.delay => 
          #check in case of quick switch between pages
          if @data('application').page == 'results' 
            $el.find('.give_opinion_button').css 'visibility', ''
        , duration  

  ##
  # Make this thing!
  render : ->
    page = @data('application').page
    my_data = @data()

    description_region_style = 
      width: 1.25 * BODY_WIDTH

    proposer = if my_data.proposal.user_id then @data("user/#{my_data.proposal.user_id}") else null

    R.div className:'proposal', key:@props.long_id, 'data-page':page,
      #description
      R.div className:'description_region', style : description_region_style, 
        R.div className: 'fixed_proposal_heading', 
          R.span null,"#{my_data.proposal.category} #{my_data.proposal.designator}: #{my_data.proposal.name}"

        R.div className: 'context',
          Avatar 
            className: 'proposer_avatar'
            key: my_data.proposal.user_id
            tag: R.img
            img_style: 'large'
            style: 
              visibility: if my_data.sticky_opinion then 'hidden' else 'visible'
          
          R.span className: 'proposal_author_tic'
          
          R.div className: 'proposer_name', "by #{if proposer then proposer.name else ''}, EFF Outreach Director" 

        R.div className: 'proposal_details', dangerouslySetInnerHTML:{__html: my_data.proposal.description}

    
      #feelings
      R.div className:'feelings_region', style : { width: BODY_WIDTH },
        OpinionFilter() #error in current activerest implementation makes you have to put components that other components depend on earlier

        Histogram()

        Slider()


      #reasons
      R.div 
        className:'reasons_region'
        style : { width: REASONS_REGION_WIDTH }, #, left: -(REASONS_REGION_WIDTH - CONTENT_WIDTH) / 2 },
          #community pros
          CommunityPoints { key: 'pros' }
            
          #your reasons
          DecisionBoard()

          #community cons
          CommunityPoints { key: 'cons' }

      if @data('application').page == 'results'
        R.div 
          className:'give_opinion_button2'
          onClick: (=> togglePage('crafting'))
          style: 
            width: BODY_WIDTH
            margin: 'auto'
        , 'Give your Opinion'


##
# Histogram
Histogram = ReactiveComponent
  displayName : 'Histogram'

  getDefaultProps : ->
    opinions: []

  ##
  # buildHistogram
  # Split up opinions into segments. For now we'll keep three hashes: 
  #   - all opinions
  #   - high level segments (the seven segments, strong supporter, neutral, etc)
  #   - small segments that represent individual columns in the histogram,
  #     now that we do not have wide bars per se
  buildHistogram : ->
    ##
    # Size the avatars. Size of avatar shrinks proportional to 1/sqrt(num_opinions)
    opinions = @data(active_proposal_key()).opinions

    avatar_size = Math.min BIGGEST_POSSIBLE_AVATAR_SIZE, 
      Math.floor(BIGGEST_POSSIBLE_AVATAR_SIZE / Math.sqrt( (opinions.length + 1) / 10 )  )

    # Calculate about how many columns of opinions to put on the histogram. 
    columns_in_histogram = Math.floor(HISTOGRAM_WIDTH / avatar_size)

    max_slider_variance = 2.0 # Slider stances vary from -1.0 to 1.0. 

    # Assign each column in the histogram to a segment. Each column is an 
    # empty array which will eventually hold opinions.
    segments = ( [] for segment in [0..6] )
    for col in [0..columns_in_histogram]
      segment = getStanceSegment(max_slider_variance * col / columns_in_histogram - 1)
      segments[segment].push []

    # ensure neutral segment has 3 columns
    segments[3].push([]) while segments[3].length < 3   

    # Assign each Opinion to a column
    # This gets complicated because we treat the extremes and Neutral differently. 
    #  - The number of columns in the extremes is variable, with the max number
    #    of opinions per columns capped. Here we'll dynamically grow the number of 
    #    cols in each extreme, subdividing the cols whenever they hit their max number. 
    #  - There are three columns for Neutral. We distributed the opinions evenly 
    #    across these three. 
    #  - Opinions belonging to other places along the spectrum are mapped directly 
    #    to the column associated with that stance.     
    opinions_in_segment = {0:0, 1:0, 2:0, 3:0, 4:0, 5:0, 6:0}
    max_opinions_in_column = Math.floor MAX_HISTOGRAM_HEIGHT / avatar_size
    for opinion in opinions
      segment = getStanceSegment opinion.stance

      # If this is a Neutral opinion, fill up all three neutral cols equally
      if segment == 3
        segments[3][opinions_in_segment[3] % 3].push opinion

      # If this is an extreme opinion...
      else if segment in [0,6]
        last_col_in_segment = segments[segment].length - 1
        if segments[segment][last_col_in_segment].length == max_opinions_in_column - 1
          segments[segment].push []

        segments[segment][last_col_in_segment].push opinion

      # If this opinion is somewhere else on the spectrum...
      else
        adjusted_stance = Math.abs(opinion.stance - stanceSegmentBoundaries[segment])
        col_width = Math.abs( stanceSegmentBoundaries[segment + 1] - 
          stanceSegmentBoundaries[segment])/segments[segment].length
        segments[segment][Math.floor(adjusted_stance / col_width)].push opinion

      opinions_in_segment[segment] += 1

    # make sure that the extreme segment with fewest opinions has as 
    # many columns as the other extreme
    if segments[0].length != segments[6].length
      if segments[0].length > 
         segments[6].length then [bigger, smaller] = [segments[0], segments[6]] else 
                                 [bigger, smaller] = [segments[6], segments[0]]
      
      all_opinions_in_smaller = _.flatten(smaller)

      smaller.push([]) while smaller.length < bigger.length

      for column in smaller
        column.pop() while column.length > 0

      for opinion, idx in all_opinions_in_smaller
        smaller[idx%smaller.length].push opinion

    num_columns = _.flatten(_.values(segments), true).length

    [num_columns, segments, avatar_size]

  onClickHistogram : (ev) ->
    page = @data('application').page
    if page == 'crafting'
      togglePage('results')

  onSelectSegment : (ev) ->
    if @data('application').page == 'results'
      segment = $(ev.currentTarget).data('segment')

      selected_segment = @data('opinionfilter').selected_segment

      # If clicking on already hard-selected segment, then we'll deselect. 
      segment = if selected_segment[0] == segment && 
                   selected_segment[1] then null else segment
      click_select = ev.type == 'click' 

      #ignore mouseEnter and mouseLeave events if selection was via click
      return if selected_segment && selected_segment[1] && !click_select 

      @data('opinionfilter').selected_segment = [segment, click_select && segment]
      ev.stopPropagation()

  render : ->
    selected_segment = @data('opinionfilter').selected_segment
    users_to_highlight = @data(active_proposal_key()).highlighted_users    
    [num_columns, segments, avatar_size] = @buildHistogram() #todo: memoize

    effective_histogram_width = num_columns * avatar_size
    margin_adjustment = -(effective_histogram_width - HISTOGRAM_WIDTH)/2
    #make sure that the neutral segment is centered
    margin_adjustment += (segments[0].length - segments[6].length) / 2 * avatar_size

    R.table 
      className: 'histogram'
      onClick:@onClickHistogram
      # onMouseLeave: @onSelectSegment
      style: { width: effective_histogram_width, marginLeft: margin_adjustment }, 
        R.tr null, 
          for bars, idx in segments.reverse()
            segment = 6 - idx
            is_selected = selected_segment[0] == segment
            R.td 
              className:"histogram_segment #{if is_selected then 'is_selected' else 'not_selected'}"
              key:segment
              # onMouseEnter: @onSelectSegment
              'data-segment':segment
              style : { opacity: (if is_selected && !selected_segment[0]? then '1' else '1') }
            ,
              R.table null,
                R.tr null,
                  for bar in bars
                    R.td className:"histogram_bar", style: {width: avatar_size},
                      for opinion in bar
                        Avatar 
                          tag: R.span, 
                          key: opinion.user_id
                          user: opinion.user_id
                          'data-segment':segment
                          style:
                            height: avatar_size
                            width: avatar_size
                            opacity: if users_to_highlight && 
                              !_.contains(users_to_highlight, opinion.user_id) 
                              then '.15' 
                              else if selected_segment[0] == null || is_selected 
                              then '1.0'
                              else '.15'
              if segment == 6
                R.div className:"histogram_segment_pole", 'Supporters'
              else if segment == 0
                R.div className:"histogram_segment_pole", 'Opposers'

##
# Slider
# Manages the slider and the UI elements attached to it. 
Slider = ReactiveComponent
  displayName: 'Slider'
  mixins: [StyleAnimator]

  getDefaultData : ->
    initial_stance = @data(active_proposal_key()).initial_stance
    return {
      has_moved : false
      stance : initial_stance
      stance_segment : getStanceSegment initial_stance
    }

  componentDidMount : -> 
    @setSlidability()

  componentWillUpdate : (prev_props, prev_state) ->
    @setSlidability()

  # We have a separate applyStyle here from Proposal because the Slider component
  # gets rerendered more frequently than the Proposal component (i.e. when the 
  # slider is being slid...forcing the entire Propoosal component to rerender
  # causes terrible performance).
  applyStyles : (animate = false) ->
    duration = if animate then TRANSITION_SPEED else 0
    $el = $(@getDOMNode())
    my_data = @data()

    mouth_scaler = if my_data.stance_segment > 3 then -1 else 1
    mouth_x = if my_data.stance_segment == 3 then 0 else -7.5

    switch @data('application').page
      when 'crafting'
        styles = 
          '.bubblemouth': { scaleX: mouth_scaler * 1.5, scaleY: 1.5, translateY: 5.5, translateX: mouth_x * 1.5  }
          '.the_handle':  { scale: 2.5, translateY: -8 }

      when 'results'
        styles = 
          '.bubblemouth': { scaleX: mouth_scaler, scaleY: 1, translateY: -6, translateX: mouth_x  }
          '.the_handle':  { scale: 1, translateY: -9 }

    @applyStylesToElements styles, duration

  ##
  # setSlidability
  # Inits jQuery UI slider and enables/disables it between pages
  setSlidability : ->
    page = @data('application').page
    $slider_base = $(@getDOMNode()).find('.slider_base')
    if $slider_base.hasClass "ui-slider"
      $slider_base.slider(if page == 'results' then 'enable' else 'enable') 
    else
      $slider_base.slider
        disabled: false #@data('application').page == 'results'
        min: -1
        max: 1
        step: .01
        value: -@data(active_proposal_key()).initial_stance
        slide: (ev, ui) => 
          # Update the stance segment if it has changed. This facilitates 
          # the feedback atop the slider changing from e.g. 'strong 
          # supporter' to 'neutral'
          return false if ev.originalEvent.type != 'mousemove'

          my_data = @data()
          my_data.stance_segment = getStanceSegment -ui.value
          my_data.stance = -ui.value
          if !my_data.has_moved
            my_data.has_moved = true
        stop : (ev, ui) =>
          if @data('application').page == 'results'
            togglePage('crafting')

  togglePage : (ev) ->
    page = if @data('application').page == 'results' then 'crafting' else 'results'
    togglePage(page)
    ev.stopPropagation()

  render : ->
    my_data = @data()
    stance = my_data.stance
    segment = my_data.stance_segment
    page = @data('application').page
    sticky_opinion = @data(active_proposal_key()).sticky_opinion

    #jquery UI slider will pick an el with this class name up
    props_for_slider_handle = { className:'ui-slider-handle' } 
    if page == 'results'    
      _.extend props_for_slider_handle, 
        onClick: => @togglePage
        style: { cursor : 'pointer' } 

    # Can't be dynamically set b/c of integration with asset fingerprinting. This is 
    # also why this file is ERB
    if segment == 3
      if page == 'crafting'
        bubblemouth_src = "<%= asset_path 'bubblemouth_neutral-crafting.png' %>"
      else
        bubblemouth_src = "<%= asset_path 'bubblemouth_neutral-results.png' %>"
    else
      if page == 'crafting'
        bubblemouth_src = "<%= asset_path 'bubblemouth-crafting.png' %>"
      else
        bubblemouth_src = "<%= asset_path 'bubblemouth-results.png' %>"

    slider_feedback_percent = 
      if segment == 3 
      then "Neutral" 
      else "#{Math.floor(Math.abs(stance) * 100)}% #{if segment > 3 then 'Support' else 'Oppose'}"

    slider_feedback_bucket = stance_names[segment]
    
    # nasty, reference to other sticky components' height...
    if sticky_opinion
      sticky_top = $('.fixed_proposal_heading').outerHeight() + 55
    ## 

    slider_style = 
      if !sticky_opinion || page == 'results' 
      then {}
      else { position: 'fixed', top: sticky_top, backgroundColor: 'white', zIndex: 100 }

    
    face_left_eye_style = 
      transform: 
        if segment in [0, 6] 
        then 'scale(.9, .8)' 
        else "scale(#{.8 + .2 * Math.abs(stance)}, #{.8 + .2 * Math.abs(stance)})"

    face_right_eye_style = 
      transform: 
        if segment in [0, 6] 
        then 'scale(1.1, 1.2)' 
        else "scale(#{.8 + .2 * Math.abs(stance)}, #{.8 + .2 * Math.abs(stance)})"

    face_mouth_style = 
      transform: 
        "scale(#{.4 + .3 * Math.abs(stance) }, #{.08 + .4 * stance}) 
         translate(0, #{-2 - 5 * Math.abs(stance)}px) 
         rotate(45deg) 
         skew(#{if segment in [0,6] then '10deg' else '0deg'})"

    # safari only respects -webkit-transform
    crossbrowserTransformProperty = (props) -> _.extend props,
      '-webkit-transform' : props.transform
      '-ms-transform' : props.transform
      '-moz-transform' : props.transform

    crossbrowserTransformProperty sty for sty in [
      face_left_eye_style, face_right_eye_style, face_mouth_style]

    # ugly access here...this is a hack that hides a dependency between slider and histogram 
    triangle_width = 
      if sticky_opinion 
      then 0 
      else $('.histogram_segment[data-segment="0"]').width() + 5
    # /ugly

    slider_base_triangle_style = 
      position: 'absolute'
      width: 0
      height: 0
      borderLeft: "#{triangle_width}px solid transparent"
      borderRight: "#{triangle_width}px solid transparent"
      borderBottom: "6px solid #787878"

    R.div className: 'slider', style: slider_style, 
      R.div className:'slider_base', style : {width: HISTOGRAM_WIDTH}, 
        R.div 
          className: 'slider_base_triangle_left'
          style: _.extend({}, slider_base_triangle_style, {left: -triangle_width})
        R.div 
          className: 'slider_base_triangle_right'
          style: _.extend({}, slider_base_triangle_style, {right: -triangle_width})
        R.div props_for_slider_handle, 
          R.div className: 'the_handle', onClick: @togglePage,
            R.div className: 'face_mouth face_part', style: face_mouth_style
            R.div className: 'face_left_eye face_eye face_part', style: face_left_eye_style
            R.div className: 'face_right_eye face_eye face_part', style: face_right_eye_style
          R.img className:'bubblemouth', src: bubblemouth_src
          R.div 
            className:'slider_feedback'
            'data-stance-segment': segment
            style: {visibility: if sticky_opinion then 'hidden' else 'visible'}
          ,
            R.div 
              className:'slider_feedback_instructions'
              style : 
                visibility: 
                  if !@data().has_moved 
                  then 'hidden' 
                  else 'visible'
              , "Your overall opinion:"
            R.div className:'slider_feedback_result', 
              if !my_data.has_moved 
              then 'Drag Your Overall Opinion' 
              else slider_feedback_bucket

##
# OpinionFilter
# Filters whose opinions are displayed
OpinionFilter = ReactiveComponent
  displayName: 'OpinionFilter'

  getDefaultData : ->
    # UI STATE
    selected_segment : [null, null]

  onSelectSegment : (segment) ->
    if @data('application').page == 'results'
      current_segment = @data().selected_segment
      @data().selected_segment = if current_segment == segment then [null, null] else [segment, segment != null]


  render : ->
    R.div className: 'opinion_filter_region',
      R.a className: "filter_option #{if @data().selected_segment[0]==null then 'selected' else ''}", onClick: (=> @onSelectSegment(null)), 'show all opinions'        
      R.a className: "filter_option #{if @data().selected_segment[0]==0 then 'selected' else ''}", onClick: (=> @onSelectSegment(0)), 'only entrenched opposers'
      R.a className: "filter_option #{if @data().selected_segment[0]==1 then 'selected' else ''}", onClick: (=> @onSelectSegment(1)), 'only firm opposers'
      R.a className: "filter_option #{if @data().selected_segment[0]==2 then 'selected' else ''}", onClick: (=> @onSelectSegment(2)), 'only leaning oppose'
      R.a className: "filter_option #{if @data().selected_segment[0]==3 then 'selected' else ''}", onClick: (=> @onSelectSegment(3)), 'only neutrals'
      R.a className: "filter_option #{if @data().selected_segment[0]==4 then 'selected' else ''}", onClick: (=> @onSelectSegment(4)), 'only leaning support'
      R.a className: "filter_option #{if @data().selected_segment[0]==5 then 'selected' else ''}", onClick: (=> @onSelectSegment(5)), 'only firm supporters'
      R.a className: "filter_option #{if @data().selected_segment[0]==6 then 'selected' else ''}", onClick: (=> @onSelectSegment(6)), 'only entrenched supporters'



##
# DecisionBoard
# Handles the user's list of important points in crafting page. 
DecisionBoard = ReactiveComponent
  displayName: 'DecisionBoard'

  componentDidMount : ->
    # make this a drop target
    $el = $(@getDOMNode())
    $el.droppable
      accept: ".point_content"
      drop : (ev, ui) =>
        if ui.draggable.parent().is('.community_point')
          proposal_data = @data(active_proposal_key())
          ui.draggable.parent().velocity 'fadeOut', 200, -> 
            included_points = proposal_data.included_points.splice(0)
            included_points.push ui.draggable.parent().data('id')
            proposal_data.included_points = included_points
          $el.removeClass "user_is_hovering_on_a_drop_target"
      out : (ev, ui) => 
        $el.removeClass("user_is_hovering_on_a_drop_target") if ui.draggable.parent().is('.community_point')
      over : (ev, ui) => 
        $el.addClass("user_is_hovering_on_a_drop_target") if ui.draggable.parent().is('.community_point')

  render : ->
    sticky_opinion = @data(active_proposal_key()).sticky_opinion
    page = @data('application').page



    sticky_header = sticky_opinion && page == 'crafting'

    # nasty, reference to other sticky components' height...
    if sticky_header
      sticky_top = $('.fixed_proposal_heading').outerHeight() + 101
    ## 

    opinion_region_style = 
      position: if sticky_header then 'fixed' else ''
      top: if sticky_header then sticky_top else ''
      left: '50%'
      width: DECISION_BOARD_WIDTH
      marginLeft: -DECISION_BOARD_WIDTH / 2

    R.div className:'opinion_region', style: opinion_region_style,
      R.div 
        className:'decision_board_body'
        onClick: => if @data('application').page == 'results' then togglePage('crafting')
      ,

        # only shown during crafting, but needs to be present always for animation
        R.div className: 'your_points',
          # your pros
          YourPoints { key: 'pros' }

          # your cons
          YourPoints { key: 'cons' }

        # only shown during results, but needs to be present always for animation
        R.a 
          className:'give_opinion_button'
        , 'Give your Opinion'

      R.div 
        className:'save_opinion_button'
        onClick: (=> togglePage('results'))
      , 'Save your opinion and see results'

      R.a 
        className:'cancel_opinion_button'
        onClick: (=> togglePage('results'))
      , 'discard your opinion'



##
# YourPoints
# List of important points for the active user. 
# Two instances used for Pro and Con columns. Shown as part of DecisionBoard. 
# Creates NewPoint instances.
YourPoints = ReactiveComponent
  displayName: 'YourPoints'

  render : ->
    included_points = @data(active_proposal_key()).included_points
    left_or_right = if @props.key == 'pros' then 'left' else 'right'

    R.div 
      className:"points_on_decision_board #{@props.key}_on_decision_board"
      style: { width: POINT_WIDTH }
    ,
      R.div className:'points_heading_label',
        "List Your #{capitalize(@props.key)}"

      R.ul null,
        for point_id in included_points
          is_pro = @data("point/#{point_id}").is_pro
          if is_pro == (@props.key == 'pros')
            Point { key: point_id, location_class: 'decision_board_point' }

        R.div className:'add_point_drop_target',
          R.div className: "drop_target_circle_#{left_or_right}"
          R.img className:'drop_target', src: "<%= asset_path 'drop_target.png' %>"
          R.span className:'drop_prompt',
            #"Drag #{@props.valence} points from the #{if @props.valence == 'pro' then 'left' else 'right'} that resonate with you."
            "Drag a #{capitalize(if @props.key=='pros' then 'pro' else 'con')} from the #{left_or_right}"

        NewPoint { key: @props.key }

##
# CommunityPoints
# List of points contributed by others. 
# Shown in wing during crafting, in middle on results. 
CommunityPoints = ReactiveComponent
  displayName: 'CommunityPoints'

  componentDidMount : ->
    @setPointMouseover()

  ## ##
  # On hovering over a point, highlight the people who included this 
  # point in the Histogram.
  setPointMouseover : ->
    $el = $(@getDOMNode())

    $el.on 'mouseenter mouseleave', '.includers', (ev) => 

      if @data('application').page == 'results'
        point_id = $(ev.currentTarget).parent().data('id')
        includers = @data("point/#{point_id}").includers
        proposal_data = @data(active_proposal_key())

        if ev.type == 'mouseenter'          
          proposal_data.highlighted_users = includers
        else if ev.type == 'mouseleave'
          _.delay =>
            if proposal_data.highlighted_users == includers
              proposal_data.highlighted_users = null
          , 200

  buildNewPointThreshold : ->
    # Grab the 10th percentile
    points = @data(active_proposal_key()).points
    newpoint_threshold = 
      (_.sortBy points, (pnt) => - Date.parse(pnt.created_at))[Math.ceil(points.length / 10)]

    (newpoint_threshold and Date.parse(newpoint_threshold.created_at)) or 
      new Date()

  buildPointsList : ->
    page = @data('application').page
    selected_segment = @data('opinionfilter').selected_segment
    proposal_data = @data(active_proposal_key())
    included_points = proposal_data.included_points
    points = proposal_data.points
    opinions = proposal_data.opinions

    points = 
      _.filter points, (pnt) =>
        is_correct_valence = pnt.is_pro == (@props.key == 'pros')
        has_not_been_included = page == 'results' || !_.contains(included_points, pnt.id)
        is_correct_valence && has_not_been_included
    
    if selected_segment[0]?
      # If there is a histogram segment selected, we'll have to filter down 
      # to the points that users in this segment think are important, and 
      # order them by resonance to those users. I'm doing this quite inefficiently.
      point_inclusions_per_point_for_segment = {} # map of points to including users
      _.each opinions, (opinion) =>
        if opinion.stance_segment == selected_segment[0] && opinion.point_inclusions
          for point_id in opinion.point_inclusions
            if !(point_id of point_inclusions_per_point_for_segment)
              point_inclusions_per_point_for_segment[point_id] = 1
            else
              point_inclusions_per_point_for_segment[point_id] += 1

      points = 
        _.filter points, (pnt) -> pnt.id of point_inclusions_per_point_for_segment
      points = 
        _.sortBy points, (pnt) -> -point_inclusions_per_point_for_segment[pnt.id]
    else
      # Default sort order
      points = 
        _.sortBy points, (pnt) => - if page == 'results' then pnt.score else pnt.persuasiveness

    points

  render : ->
    selected_segment = @data('opinionfilter').selected_segment
    page = @data('application').page

    #filter to pros or cons & down to points that haven't been included
    points = @buildPointsList() #todo: memoize
    newpoint_threshold = @buildNewPointThreshold() #todo: memoize
    label = capitalize @props.key

    R.div 
      className:"points_by_community #{@props.key}_by_community"
      style: { width: POINT_CONTENT_WIDTH, minHeight: jQuery(window).height() }
    , 
      R.div className:'points_heading_label', 
        if page == 'results' then "Top #{label}" else "Others' #{label}"
        R.p className:'points_segment_label', style: {visibility: if selected_segment[0] == null then 'hidden' else 'visible'},
          if selected_segment[0] == null then '-' else "for #{stance_names[selected_segment[0]]}s"          

      R.ul null, 
        if points.length > 0
          for point in points
            Point 
              key: point.id, 
              is_new: Date.parse(point.created_at) > newpoint_threshold
              location_class : 'community_point'
        else
          R.li className: 'empty_point', "No #{label} listed"

##
# Point
# A single point in a list. 
Point = ReactiveComponent
  displayName: 'Point'

  getDefaultData : ->
    show_details : false

  componentDidMount : ->
    @setShowDetails()
    @setDraggability()

  componentDidUpdate : -> 
    @setDraggability()

  setDraggability : ->
    # Ability to drag include this point if a community point, 
    # or drag remove for point on decision board
    # also: disable for results page

    disable = @data('application').page == 'results'
    $point_content = $(@getDOMNode()).find('.point_content')
    revert = 
      if @props.location_class == 'community_point' 
      then 'invalid' 
      else (valid) =>
        if !valid
          included_points = 
            _.without @data(active_proposal_key()).included_points, @props.key
          @data(active_proposal_key()).included_points = included_points
        valid

    if $point_content.hasClass "ui-draggable"
      $point_content.draggable(if disable then 'disable' else 'enable') 
    else
      $point_content.draggable
        revert: revert
        disabled: disable

  setShowDetails : ->
    $(@getDOMNode()).click (ev) => @data().show_details = !@data().show_details

  buildIncluders : -> 
    my_data = @data()
    to_remove = [my_data.user_id]

    # only show includers from this stance segment if a segment is selected
    selected_segment = @data('opinionfilter').selected_segment
    if selected_segment[0]?
      for includer in my_data.includers
        stance_segment = @data("opinion/#{includer}").stance_segment
        if stance_segment && stance_segment != selected_segment[0]
          to_remove.push(includer)

    _.difference my_data.includers, to_remove

        
  renderIncluders : ->

    if @data().includers

      includers = @buildIncluders()

      s = #includers_style
        rows: 8
        dx: 2
        dy: 5
        col_gap: 8
        side_offset: 32 + 14 + 1

      # Now we'll go through the list from back to front
      i = includers.length

      for includer in includers
        i -= 1
        curr_column = Math.floor(i / s.rows)
        side_offset = s.side_offset + curr_column*s.col_gap + i*s.dx
        top_offset = (i % s.rows) * s.dy + 4
        left_right = if @data().is_pro then 'left' else 'right'
        style = { top: top_offset }
        style[left_right] = -side_offset

        # Finally draw the guy
        Avatar
          key: includer
          tag: R.span
          className: "point_includer_avatar"
          style: style

  renderNewIndicator : ->
    if @data().includers
      side_offset = 51
      left_right = if @data().is_pro then 'left' else 'right'
      style = 
        position: 'absolute'
        color: 'rgb(255,22,3)'
        fontSize: '11px'
        top:'41px'
        backgroundColor: 'white', zIndex: 5
      style[left_right] = "#{-side_offset}"
      R.span {style: style}, '*New*'

  render : -> 
    my_data = @data()
    selected_segment = @data('opinionfilter').selected_segment
    author_opinion = @data("opinion/#{my_data.user_id}")

    R.li 
      className: "point closed_point #{@props.location_class} 
        #{if my_data.is_pro then 'pro' else 'con'}"
      'data-id':@props.key
    ,
  
      if @props.location_class == 'community_point' && @props.is_new
        @renderNewIndicator()

      R.div className:'includers', 
        if @props.location_class == 'community_point'
          @renderIncluders()


        if !selected_segment[0] || ( author_opinion && author_opinion.stance_segment == selected_segment[0] )
          Avatar tag: R.span, key: my_data.user_id, className:"point_author_avatar"

      R.div className:'point_content', style : { width: POINT_CONTENT_WIDTH },
        if @props.location_class == 'community_point'
          R.img 
            className: 'community_point_mouth'
            src: "<%= asset_path 'community_point_mouth.png' %>"

        R.div className:'point_nutshell',
          my_data.nutshell
          if my_data.text
            if my_data.show_details or 
              (my_data.nutshell.length + my_data.text.length) < 210
                R.div className: 'point_details', dangerouslySetInnerHTML:{__html: my_data.text}
            else
              R.span className: 'point_details_tease', 
                $("<span>#{my_data.text[0..210-my_data.nutshell.length]}</span>").text() + " >>"

        # R.a className:'open_point_link',
        #   "#{@props.comment_count} comment#{if @props.comment_count != 1 then 's' else ''}"

##
# NewPoint
# Handles adding a new point into the system. Only rendered when proposal is
# in Crafting state. Manages whether the user has clicked "add a new point". 
# If they have, show new point form. 
NewPoint = ReactiveComponent
  displayName: 'NewPoint'

  getDefaultData : ->
    # UI STATE
    edit_mode : false

  handleAddPointBegin : (ev) -> @data().edit_mode = true
  handleAddPointCancel : (ev) -> @data().edit_mode = false

  handleSubmitNewPoint : (ev) ->
    $form = $(@getDOMNode())

    current_user_id = -2 #anon user
    point =
      id : ActiveREST.getUniqueID 'point'
      nutshell : $form.find('#nutshell').val()
      text : $form.find('#text').val()
      is_pro : @props.key == 'pros'
      user_id : current_user_id
      comment_count : 0 
      includers : [current_user_id]

    proposal_data = @data active_proposal_key()
    points = proposal_data.points.splice(0)
    points.push point
    proposal_data.points = points

    # how to not push to multiple places...
    ActiveREST.save "point/#{point.id}", point
 
    included_points = proposal_data.included_points.splice(0)
    included_points.push point.id
    proposal_data.included_points = included_points

    @data().edit_mode = false

  render : ->
    #TODO: refactor HTML/CSS for new point after we get better sense of new point redesign

    R.div className:'newpoint',
      if !@data().edit_mode
        R.div className:'newpoint_prompt',
          R.span className:'qualifier', 
            'or '
          R.span className:'newpoint_bullet', dangerouslySetInnerHTML:{__html: '&bull;'}
          R.a className:'newpoint_link', 'data-action':'write-point', onClick: @handleAddPointBegin,
            "Write a new #{capitalize(if @props.key == 'pros' then 'pro' else 'con')}"
      else
        R.div className:'newpoint_form',
          R.input id:'is_pro', name: 'is_pro', type: 'hidden', value: "#{@props.key == 'pros'}"
          R.div className:'newpoint_nutshell_wrap',
            R.textarea id:'nutshell', className:'newpoint_nutshell is_counted', cols:'28', maxLength:"140", name:'nutshell', pattern:'^.{3,}', placeholder:'Summarize your point (required)', required:'required'
            R.span className: 'count', 140
          R.div className:'newpoint_description_wrap',
            R.textarea id:'text', className:'newpoint_description', cols:'28', name:'text', placeholder:'Write a longer description (optional)', required:'required'
          R.div className:'newpoint_hide_name',
            R.input className:'newpoint-anonymous', type:'checkbox', id:"hide_name-#{@props.key}", name:"hide_name-#{@props.key}"
            R.label for:"hide_name-#{@props.key}", title:'We encourage you not to hide your name from other users. Signing your point with your name lends it more weight to other participants.', 
              'Conceal your name'
          R.div className:'newpoint-submit',
            R.a className:'newpoint-cancel', onClick: @handleAddPointCancel,
              'cancel'
            R.input className:'button', action:'submit-point', type:'submit', value:'Done', onClick: @handleSubmitNewPoint

##
# Avatar
# Displays a user's avatar
# Supports straight up img src, or using the CSS-embedded b64 for each user
Avatar = ReactiveComponent
  has_multiple_instances_with_same_key: true
  displayName: 'Avatar'
  
  getDefaultData: -> 
    show_details : false

  getDefaultProps : ->
    key: -1 # defaults to anonymous user
    tag: R.img
    img_style: null #null defaults to the css-based b64 embedded images

  componentWillUpdate : -> 
    if @data().show_details
      @insertUserName()

  insertUserName : ->
    $el = $(@getDOMNode())
    pos = $el.offset()

    $name = $("<span class='hover_avatar_name'>#{@data("user/#{@props.key}").name}</span>")
    $('body').append $name   # ugly!

    $name.css 
      left: pos.left - $name.width() / 2 + $el.width()/2
      top: pos.top - $name.height()

  showDetails : (ev) -> 
    @data().show_details = true

  hideDetails : (ev) -> 
    $('.hover_avatar_name').remove()
    @data().show_details = false

  render : ->
    show_details = @data().show_details
    className = "#{@props.className} avatar"
    id = "avatar-#{@props.key}"

    if @props.img_style && @props.key != -1
      avatar_file_name = @data("user/#{@props.key}").avatar_file_name

      if avatar_file_name
        filename = "/system/avatars/#{@props.key}/#{@props.img_style}/#{avatar_file_name}"

    attrs = 
      className: className
      id: id
      'data-id': @props.key
      onMouseEnter: @showDetails
      onMouseLeave: @hideDetails

    attrs.src = filename if filename
    

    @transferPropsTo @props.tag attrs


Application = ReactiveComponent
  displayName: 'Application'

  getDefaultData : ->
    page : @props.initial_page
    route : "/#{@props.long_id}#{if @props.initial_page == 'results' then '/results' else ''}"

  render : -> 
    R.div null,
      R.div className: 'l_header'
      R.div id: 'body', style : { minWidth: PAGE_WIDTH }, 
        R.div id: 'inner_body', #style : { width: CONTENT_WIDTH },
          Proposal { key: @props.long_id }
      R.div className: 'l_footer'


# Initialize ActiveREST with some knowledge of server endpoints
# and what to do with the data. 
ActiveREST = new ActiveRESTCache 
  routes : 
    'proposal/\\w{10}' : 
      get : 
        success : (key, proposal_data) =>
          data_to_save = []

          proposal_data.users = $.parseJSON proposal_data.users
          for user in proposal_data.users
            data_to_save.push ["user/#{user.id}", user]

          for point in proposal_data.points
            point.includers = $.parseJSON point.includers
            data_to_save.push ["point/#{point.id}", point]

          for opinion in proposal_data.opinions
            opinion.point_inclusions = $.parseJSON opinion.point_inclusions
            data_to_save.push ["opinion/#{opinion.user_id}", opinion]

          #I don't like passing key outside ActiveREST...
          data_to_save.push [key, proposal_data] 

          data_to_save

      save : {}

window.ActiveREST = ActiveREST

## ########################
## Application area

##
# load users' pictures
$.get Routes.get_avatars_path(), (data) -> $('head').append data

##
# Backbone routing
# Note: not committed to backbone. Want to experiment with other routing techniques too.
top_level_component = null
Router = Backbone.Router.extend

  routes :
    ":proposal(/)": "proposal"
    ":proposal/results(/)": "results"

  proposal : (long_id, page = 'crafting') ->

    if !top_level_component
      top_level_component = 
        React.renderComponent Application({long_id : long_id, initial_page : page}), document.getElementById('content')
    else
      app_data = ActiveREST.fetch 'application'
      _.extend app_data, 
        route : "/#{long_id}#{if page == 'results' then '/results' else ''}"
        page : page
      ActiveREST.save 'application', app_data, ['route', 'page']

  results : (long_id) -> @proposal long_id, 'results'

window.app_router = new Router()

$(document).ready -> Backbone.history.start {pushState: true}
<commit_msg>more explicit "read more" for points<commit_after>#////////////////////////////////////////////////////////////
# Exploratory reimplementation of considerit client in React
#////////////////////////////////////////////////////////////

# Ugliness in this prototype: 
#   - Provision of @data() as a function via reactiveREST, rather than a straight up object. I couldn't
#     figure out a way to do it more cleanly. 
#   - Children needing to know the parent Proposal's key, and using active_proposal_key() to get it
#   - Programmer needing to know multiple ActiveREST cache keys to save new points to
#   - UI state and persisted data stored in same place (when saving, what to send to server?)
#   - Figuring out if the route was changed. Or more generally, knowing what the diff in state was
#     in the lifecycle methods now that we're not using @props or @state. Right now using a hack
#     (see use of _route_changed ActiveREST cache key)
#   - All UI state _must_ be declared in the @getDefaultData method for the component at which
#     that state will be stored. Otherwise, getters and setters won't be operative!
#   - Computed state not yet stored in ActiveREST
#   - Easy for programmer to accidently mess up ActiveREST cache
#   - Managing top_level_component in Router
#   - NewPoint CSS/HTML is still bulky, waiting on redesign

# ##
# React aliases
R = React.DOM

# ##
# Constants, especially used for layout styling
TRANSITION_SPEED = 700   # Speed of transition from results to crafting (and vice versa) 
BIGGEST_POSSIBLE_AVATAR_SIZE = 50
GRID_MAJOR = 48  #GRID_MAJOR = GRID_MINOR + GRID_GUTTER
GRID_MINOR = 30
GRID_GUTTER = 18
PAGE_WIDTH = 24 * GRID_MAJOR
CONTENT_WIDTH = PAGE_WIDTH - 4 * GRID_MAJOR
BODY_WIDTH = CONTENT_WIDTH - 8 * GRID_MAJOR - 2 * GRID_GUTTER
POINT_WIDTH = BODY_WIDTH / 2 - 20
POINT_CONTENT_WIDTH = POINT_WIDTH - 35 - GRID_GUTTER
HISTOGRAM_WIDTH = BODY_WIDTH    # Width of the slider / histogram base 
DECISION_BOARD_WIDTH = BODY_WIDTH + 4 # the four is for the border
REASONS_REGION_WIDTH = DECISION_BOARD_WIDTH + 2 * POINT_CONTENT_WIDTH + 4 * GRID_GUTTER + 4
MAX_HISTOGRAM_HEIGHT = 200


##
# Helpers that should probably go elsewhere

capitalize = (string) -> string.charAt(0).toUpperCase() + string.substring(1)

# Returns the top level Proposal component key. Components use this to grab data
# from its Proposal. This is a hack. Shadowing parent state would solve the problem.
active_proposal_key = -> 
  "proposal/#{ActiveREST.fetch('application').route.split('/')[1]}"

getStanceSegment = (value) ->
  for i in [0..5]
    if value < stanceSegmentBoundaries[i + 1]
      return i
  return 6

togglePage = (page) -> 
  $('.histogram_segment_pole').ensureInView
    offset_buffer: 150
    callback : -> 
      top_level_component.data('application').page = page


stanceSegmentBoundaries = { 0 : -1, 1 : -.9999, 2 : -0.5, 3 : -0.05, 4 : 0.05, 5 : 0.5, 6 : .9999 } 

stance_names = 
  6 : 'Entrenched Supporter'
  0 : 'Entrenched Opposer'
  5 : 'Firmly Support'
  1 : 'Firmly Oppose'
  4 : 'Lean Support'
  2 : 'Lean Oppose'
  3 : 'Neutral'

##
# StyleAnimator mixin
# Helper for components that implement animations.
# Applies styles defined in the component that change
# depending on state. Right now this mixin assumes that
# animations will be applied only on route changes. 
#
# A component that implements this mixin should define a method:
#     applyStyles : (animate = true)
# which puts the styles on the desired elements. 
StyleAnimator = 
  componentDidMount : -> @applyStyles false

  componentDidUpdate : (prev_props, prev_page) -> 
    @applyStyles ActiveREST.fetch('_route_changed')

  applyStylesToElements : (styles, duration) ->
    $el = $(@getDOMNode())
    _.each _.keys(styles), (selector) -> 
      # Only apply styles if there are differences, for performance. 
      # This check doesn't work for transform properties because for Velocity we 
      # specify them as e.g. TranslateX: 4 rather than transform: translateX(4)
      styles_for_selector = styles[selector]
      styles_to_apply = {}
      $target = $el.find(selector)

      prop_map = $target.css(_.keys(styles_for_selector))
      if prop_map
        for property in _.keys(styles_for_selector)
          if prop_map[property] != styles_for_selector[property]
            styles_to_apply[property] = styles_for_selector[property]

      if _.size(styles_to_apply) > 0
        $target.velocity styles_to_apply, {duration}


## ##################
# React Components
#
# These are the components and their relationships:
#
#                     Application 
#                          |
#                       Proposal
#                   /      |           \            \
#    CommunityPoints   DecisionBoard   Histogram    Slider
#               |          |
#               |      YourPoints
#               |    /            \
#              Point             NewPoint
#
#   (also, Proposal, Histogram, and Point employ Avatar)


##
# Proposal
# Has proposal description, feelings area (slider + histogram), and reasons area
Proposal = ReactiveComponent
  mixins: [StyleAnimator]
  displayName: 'Proposal'

  ##
  # Component defaults

  getDefaultData : ->
    proposal : {}
    points : {}
    users : {}
    included_points : []
    initial_stance : 0.0
    opinions : []
    # UI STATE
    highlighted_users : null
    sticky_opinion : false
    sticky_proposal_title: false

  ##
  # Lifecycle methods

  componentDidMount : (prev_props, prev_state) ->

  componentDidUpdate : ->
    @setStickyHeader()

    $el = $(@getDOMNode())
    $el.find('.description_region').css
      top: $el.find('.fixed_proposal_heading').height() + 24


  setStickyHeader : ->
    # Sticky decision board. 
    _.delay => 
      $el = $(@getDOMNode())
      $sticky_head_el = $el.find('.slider_base')
      $(@getDOMNode()).headroom
        offset: $sticky_head_el.offset().top - $el.find('.fixed_proposal_heading').outerHeight() - 55 #+ $sticky_head_el.width() / 2 - 60
        onNotTop : => @data().sticky_opinion = true
        onTop : => @data().sticky_opinion = false
    , 2000  # delay initialization to let the rest of the dom load 
            # so that the offset is calculated properly

    # # Sticky proposal title
    # _.delay => 
    #   $sticky_head_el = $(@getDOMNode()).find('.proposal_heading')
    #   $(@getDOMNode()).find('.description_region').headroom
    #     offset: $sticky_head_el.offset().top + 25
    #     onNotTop : => 
    #       @data().sticky_proposal_title = true
    #     onTop : => @data().sticky_proposal_title = false
    # , 2000  # delay initialization to let the rest of the dom load 
    #         # so that the offset is calculated properly

    

  ##
  # State-dependent styling
  applyStyles : (animate = true) ->  
    $el = $(@getDOMNode())
    duration = if animate then TRANSITION_SPEED else 0
    page = @data('application').page

    # • Velocity requires properties to be pulled out (e.g. paddingLeft, 
    #   translateX, rather than using padding or transform)
    # • We use velocity even for 0 duration style applications to maintain 
    #   parity of style definition
    switch page
      when 'crafting'
        styles = 
          '.histogram_bar':        { opacity: '.2' }
          '.opinion_region':       { translateX: 0, translateY: 0 }
          '.decision_board_body':  { width: "#{DECISION_BOARD_WIDTH}px", minHeight: "375px"}
          '.pros_by_community':    { translateX: 0 }
          '.cons_by_community':    { translateX: "#{DECISION_BOARD_WIDTH}px" }
        
        @applyStylesToElements styles, duration

        $el.find('.give_opinion_button').css 'visibility', 'hidden'
        _.delay => 
          #delay & check in case of quick switch between pages
          if @data('application').page == 'crafting'         
            $el.find('.your_points, .save_opinion_button, .cancel_opinion_button').css 'display', ''
        , duration

      when 'results'
        slider_stance = $el.find('.ui-slider').slider('value')
        segment = getStanceSegment(-1 * slider_stance)

        opinion_region_x = DECISION_BOARD_WIDTH * (slider_stance+1) / 2.0
        give_opinion_button_width = 186
        if segment == 3
          opinion_region_x -= give_opinion_button_width / 2
        else if segment < 3
          opinion_region_x -= give_opinion_button_width * .95 
        else 
          opinion_region_x -= give_opinion_button_width * .05 


        styles = 
          '.histogram_bar':         { opacity: '1' }
          '.opinion_region':        { translateX: opinion_region_x, translateY: -18 }
          '.decision_board_body':   { width: "#{give_opinion_button_width}px", minHeight: "32px"}
          '.pros_by_community':     { translateX:  DECISION_BOARD_WIDTH / 2 }
          '.cons_by_community':     { translateX:  DECISION_BOARD_WIDTH / 2 }
        
        @applyStylesToElements styles, duration

        $el.find('.your_points, .save_opinion_button, .cancel_opinion_button').css 'display', 'none'
        _.delay => 
          #check in case of quick switch between pages
          if @data('application').page == 'results' 
            $el.find('.give_opinion_button').css 'visibility', ''
        , duration  

  ##
  # Make this thing!
  render : ->
    page = @data('application').page
    my_data = @data()

    description_region_style = 
      width: 1.25 * BODY_WIDTH

    proposer = if my_data.proposal.user_id then @data("user/#{my_data.proposal.user_id}") else null

    R.div className:'proposal', key:@props.long_id, 'data-page':page,
      #description
      R.div className:'description_region', style : description_region_style, 
        R.div className: 'fixed_proposal_heading', 
          R.span null,"#{my_data.proposal.category} #{my_data.proposal.designator}: #{my_data.proposal.name}"

        R.div className: 'context',
          Avatar 
            className: 'proposer_avatar'
            key: my_data.proposal.user_id
            tag: R.img
            img_style: 'large'
            style: 
              visibility: if my_data.sticky_opinion then 'hidden' else 'visible'
          
          R.span className: 'proposal_author_tic'
          
          R.div className: 'proposer_name', "by #{if proposer then proposer.name else ''}, EFF Outreach Director" 

        R.div className: 'proposal_details', dangerouslySetInnerHTML:{__html: my_data.proposal.description}

    
      #feelings
      R.div className:'feelings_region', style : { width: BODY_WIDTH },
        OpinionFilter() #error in current activerest implementation makes you have to put components that other components depend on earlier

        Histogram()

        Slider()


      #reasons
      R.div 
        className:'reasons_region'
        style : { width: REASONS_REGION_WIDTH }, #, left: -(REASONS_REGION_WIDTH - CONTENT_WIDTH) / 2 },
          #community pros
          CommunityPoints { key: 'pros' }
            
          #your reasons
          DecisionBoard()

          #community cons
          CommunityPoints { key: 'cons' }

      if @data('application').page == 'results'
        R.div 
          className:'give_opinion_button2'
          onClick: (=> togglePage('crafting'))
          style: 
            width: BODY_WIDTH
            margin: 'auto'
        , 'Give your Opinion'


##
# Histogram
Histogram = ReactiveComponent
  displayName : 'Histogram'

  getDefaultProps : ->
    opinions: []

  ##
  # buildHistogram
  # Split up opinions into segments. For now we'll keep three hashes: 
  #   - all opinions
  #   - high level segments (the seven segments, strong supporter, neutral, etc)
  #   - small segments that represent individual columns in the histogram,
  #     now that we do not have wide bars per se
  buildHistogram : ->
    ##
    # Size the avatars. Size of avatar shrinks proportional to 1/sqrt(num_opinions)
    opinions = @data(active_proposal_key()).opinions

    avatar_size = Math.min BIGGEST_POSSIBLE_AVATAR_SIZE, 
      Math.floor(BIGGEST_POSSIBLE_AVATAR_SIZE / Math.sqrt( (opinions.length + 1) / 10 )  )

    # Calculate about how many columns of opinions to put on the histogram. 
    columns_in_histogram = Math.floor(HISTOGRAM_WIDTH / avatar_size)

    max_slider_variance = 2.0 # Slider stances vary from -1.0 to 1.0. 

    # Assign each column in the histogram to a segment. Each column is an 
    # empty array which will eventually hold opinions.
    segments = ( [] for segment in [0..6] )
    for col in [0..columns_in_histogram]
      segment = getStanceSegment(max_slider_variance * col / columns_in_histogram - 1)
      segments[segment].push []

    # ensure neutral segment has 3 columns
    segments[3].push([]) while segments[3].length < 3   

    # Assign each Opinion to a column
    # This gets complicated because we treat the extremes and Neutral differently. 
    #  - The number of columns in the extremes is variable, with the max number
    #    of opinions per columns capped. Here we'll dynamically grow the number of 
    #    cols in each extreme, subdividing the cols whenever they hit their max number. 
    #  - There are three columns for Neutral. We distributed the opinions evenly 
    #    across these three. 
    #  - Opinions belonging to other places along the spectrum are mapped directly 
    #    to the column associated with that stance.     
    opinions_in_segment = {0:0, 1:0, 2:0, 3:0, 4:0, 5:0, 6:0}
    max_opinions_in_column = Math.floor MAX_HISTOGRAM_HEIGHT / avatar_size
    for opinion in opinions
      segment = getStanceSegment opinion.stance

      # If this is a Neutral opinion, fill up all three neutral cols equally
      if segment == 3
        segments[3][opinions_in_segment[3] % 3].push opinion

      # If this is an extreme opinion...
      else if segment in [0,6]
        last_col_in_segment = segments[segment].length - 1
        if segments[segment][last_col_in_segment].length == max_opinions_in_column - 1
          segments[segment].push []

        segments[segment][last_col_in_segment].push opinion

      # If this opinion is somewhere else on the spectrum...
      else
        adjusted_stance = Math.abs(opinion.stance - stanceSegmentBoundaries[segment])
        col_width = Math.abs( stanceSegmentBoundaries[segment + 1] - 
          stanceSegmentBoundaries[segment])/segments[segment].length
        segments[segment][Math.floor(adjusted_stance / col_width)].push opinion

      opinions_in_segment[segment] += 1

    # make sure that the extreme segment with fewest opinions has as 
    # many columns as the other extreme
    if segments[0].length != segments[6].length
      if segments[0].length > 
         segments[6].length then [bigger, smaller] = [segments[0], segments[6]] else 
                                 [bigger, smaller] = [segments[6], segments[0]]
      
      all_opinions_in_smaller = _.flatten(smaller)

      smaller.push([]) while smaller.length < bigger.length

      for column in smaller
        column.pop() while column.length > 0

      for opinion, idx in all_opinions_in_smaller
        smaller[idx%smaller.length].push opinion

    num_columns = _.flatten(_.values(segments), true).length

    [num_columns, segments, avatar_size]

  onClickHistogram : (ev) ->
    page = @data('application').page
    if page == 'crafting'
      togglePage('results')

  onSelectSegment : (ev) ->
    if @data('application').page == 'results'
      segment = $(ev.currentTarget).data('segment')

      selected_segment = @data('opinionfilter').selected_segment

      # If clicking on already hard-selected segment, then we'll deselect. 
      segment = if selected_segment[0] == segment && 
                   selected_segment[1] then null else segment
      click_select = ev.type == 'click' 

      #ignore mouseEnter and mouseLeave events if selection was via click
      return if selected_segment && selected_segment[1] && !click_select 

      @data('opinionfilter').selected_segment = [segment, click_select && segment]
      ev.stopPropagation()

  render : ->
    selected_segment = @data('opinionfilter').selected_segment
    users_to_highlight = @data(active_proposal_key()).highlighted_users    
    [num_columns, segments, avatar_size] = @buildHistogram() #todo: memoize

    effective_histogram_width = num_columns * avatar_size
    margin_adjustment = -(effective_histogram_width - HISTOGRAM_WIDTH)/2
    #make sure that the neutral segment is centered
    margin_adjustment += (segments[0].length - segments[6].length) / 2 * avatar_size

    R.table 
      className: 'histogram'
      onClick:@onClickHistogram
      # onMouseLeave: @onSelectSegment
      style: { width: effective_histogram_width, marginLeft: margin_adjustment }, 
        R.tr null, 
          for bars, idx in segments.reverse()
            segment = 6 - idx
            is_selected = selected_segment[0] == segment
            R.td 
              className:"histogram_segment #{if is_selected then 'is_selected' else 'not_selected'}"
              key:segment
              # onMouseEnter: @onSelectSegment
              'data-segment':segment
              style : { opacity: (if is_selected && !selected_segment[0]? then '1' else '1') }
            ,
              R.table null,
                R.tr null,
                  for bar in bars
                    R.td className:"histogram_bar", style: {width: avatar_size},
                      for opinion in bar
                        Avatar 
                          tag: R.span, 
                          key: opinion.user_id
                          user: opinion.user_id
                          'data-segment':segment
                          style:
                            height: avatar_size
                            width: avatar_size
                            opacity: if users_to_highlight && 
                              !_.contains(users_to_highlight, opinion.user_id) 
                              then '.15' 
                              else if selected_segment[0] == null || is_selected 
                              then '1.0'
                              else '.15'
              if segment == 6
                R.div className:"histogram_segment_pole", 'Supporters'
              else if segment == 0
                R.div className:"histogram_segment_pole", 'Opposers'

##
# Slider
# Manages the slider and the UI elements attached to it. 
Slider = ReactiveComponent
  displayName: 'Slider'
  mixins: [StyleAnimator]

  getDefaultData : ->
    initial_stance = @data(active_proposal_key()).initial_stance
    return {
      has_moved : false
      stance : initial_stance
      stance_segment : getStanceSegment initial_stance
    }

  componentDidMount : -> 
    @setSlidability()

  componentWillUpdate : (prev_props, prev_state) ->
    @setSlidability()

  # We have a separate applyStyle here from Proposal because the Slider component
  # gets rerendered more frequently than the Proposal component (i.e. when the 
  # slider is being slid...forcing the entire Propoosal component to rerender
  # causes terrible performance).
  applyStyles : (animate = false) ->
    duration = if animate then TRANSITION_SPEED else 0
    $el = $(@getDOMNode())
    my_data = @data()

    mouth_scaler = if my_data.stance_segment > 3 then -1 else 1
    mouth_x = if my_data.stance_segment == 3 then 0 else -7.5

    switch @data('application').page
      when 'crafting'
        styles = 
          '.bubblemouth': { scaleX: mouth_scaler * 1.5, scaleY: 1.5, translateY: 5.5, translateX: mouth_x * 1.5  }
          '.the_handle':  { scale: 2.5, translateY: -8 }

      when 'results'
        styles = 
          '.bubblemouth': { scaleX: mouth_scaler, scaleY: 1, translateY: -6, translateX: mouth_x  }
          '.the_handle':  { scale: 1, translateY: -9 }

    @applyStylesToElements styles, duration

  ##
  # setSlidability
  # Inits jQuery UI slider and enables/disables it between pages
  setSlidability : ->
    page = @data('application').page
    $slider_base = $(@getDOMNode()).find('.slider_base')
    if $slider_base.hasClass "ui-slider"
      $slider_base.slider(if page == 'results' then 'enable' else 'enable') 
    else
      $slider_base.slider
        disabled: false #@data('application').page == 'results'
        min: -1
        max: 1
        step: .01
        value: -@data(active_proposal_key()).initial_stance
        slide: (ev, ui) => 
          # Update the stance segment if it has changed. This facilitates 
          # the feedback atop the slider changing from e.g. 'strong 
          # supporter' to 'neutral'
          return false if ev.originalEvent.type != 'mousemove'

          my_data = @data()
          my_data.stance_segment = getStanceSegment -ui.value
          my_data.stance = -ui.value
          if !my_data.has_moved
            my_data.has_moved = true
        stop : (ev, ui) =>
          if @data('application').page == 'results'
            togglePage('crafting')

  togglePage : (ev) ->
    page = if @data('application').page == 'results' then 'crafting' else 'results'
    togglePage(page)
    ev.stopPropagation()

  render : ->
    my_data = @data()
    stance = my_data.stance
    segment = my_data.stance_segment
    page = @data('application').page
    sticky_opinion = @data(active_proposal_key()).sticky_opinion

    #jquery UI slider will pick an el with this class name up
    props_for_slider_handle = { className:'ui-slider-handle' } 
    if page == 'results'    
      _.extend props_for_slider_handle, 
        onClick: => @togglePage
        style: { cursor : 'pointer' } 

    # Can't be dynamically set b/c of integration with asset fingerprinting. This is 
    # also why this file is ERB
    if segment == 3
      if page == 'crafting'
        bubblemouth_src = "<%= asset_path 'bubblemouth_neutral-crafting.png' %>"
      else
        bubblemouth_src = "<%= asset_path 'bubblemouth_neutral-results.png' %>"
    else
      if page == 'crafting'
        bubblemouth_src = "<%= asset_path 'bubblemouth-crafting.png' %>"
      else
        bubblemouth_src = "<%= asset_path 'bubblemouth-results.png' %>"

    slider_feedback_percent = 
      if segment == 3 
      then "Neutral" 
      else "#{Math.floor(Math.abs(stance) * 100)}% #{if segment > 3 then 'Support' else 'Oppose'}"

    slider_feedback_bucket = stance_names[segment]
    
    # nasty, reference to other sticky components' height...
    if sticky_opinion
      sticky_top = $('.fixed_proposal_heading').outerHeight() + 55
    ## 

    slider_style = 
      if !sticky_opinion || page == 'results' 
      then {}
      else { position: 'fixed', top: sticky_top, backgroundColor: 'white', zIndex: 100 }

    
    face_left_eye_style = 
      transform: 
        if segment in [0, 6] 
        then 'scale(.9, .8)' 
        else "scale(#{.8 + .2 * Math.abs(stance)}, #{.8 + .2 * Math.abs(stance)})"

    face_right_eye_style = 
      transform: 
        if segment in [0, 6] 
        then 'scale(1.1, 1.2)' 
        else "scale(#{.8 + .2 * Math.abs(stance)}, #{.8 + .2 * Math.abs(stance)})"

    face_mouth_style = 
      transform: 
        "scale(#{.4 + .3 * Math.abs(stance) }, #{.08 + .4 * stance}) 
         translate(0, #{-2 - 5 * Math.abs(stance)}px) 
         rotate(45deg) 
         skew(#{if segment in [0,6] then '10deg' else '0deg'})"

    # safari only respects -webkit-transform
    crossbrowserTransformProperty = (props) -> _.extend props,
      '-webkit-transform' : props.transform
      '-ms-transform' : props.transform
      '-moz-transform' : props.transform

    crossbrowserTransformProperty sty for sty in [
      face_left_eye_style, face_right_eye_style, face_mouth_style]

    # ugly access here...this is a hack that hides a dependency between slider and histogram 
    triangle_width = 
      if sticky_opinion 
      then 0 
      else $('.histogram_segment[data-segment="0"]').width() + 5
    # /ugly

    slider_base_triangle_style = 
      position: 'absolute'
      width: 0
      height: 0
      borderLeft: "#{triangle_width}px solid transparent"
      borderRight: "#{triangle_width}px solid transparent"
      borderBottom: "6px solid #787878"

    R.div className: 'slider', style: slider_style, 
      R.div className:'slider_base', style : {width: HISTOGRAM_WIDTH}, 
        R.div 
          className: 'slider_base_triangle_left'
          style: _.extend({}, slider_base_triangle_style, {left: -triangle_width})
        R.div 
          className: 'slider_base_triangle_right'
          style: _.extend({}, slider_base_triangle_style, {right: -triangle_width})
        R.div props_for_slider_handle, 
          R.div className: 'the_handle', onClick: @togglePage,
            R.div className: 'face_mouth face_part', style: face_mouth_style
            R.div className: 'face_left_eye face_eye face_part', style: face_left_eye_style
            R.div className: 'face_right_eye face_eye face_part', style: face_right_eye_style
          R.img className:'bubblemouth', src: bubblemouth_src
          R.div 
            className:'slider_feedback'
            'data-stance-segment': segment
            style: {visibility: if sticky_opinion then 'hidden' else 'visible'}
          ,
            R.div 
              className:'slider_feedback_instructions'
              style : 
                visibility: 
                  if !@data().has_moved 
                  then 'hidden' 
                  else 'visible'
              , "Your overall opinion:"
            R.div className:'slider_feedback_result', 
              if !my_data.has_moved 
              then 'Drag Your Overall Opinion' 
              else slider_feedback_bucket

##
# OpinionFilter
# Filters whose opinions are displayed
OpinionFilter = ReactiveComponent
  displayName: 'OpinionFilter'

  getDefaultData : ->
    # UI STATE
    selected_segment : [null, null]

  onSelectSegment : (segment) ->
    if @data('application').page == 'results'
      current_segment = @data().selected_segment
      @data().selected_segment = if current_segment == segment then [null, null] else [segment, segment != null]


  render : ->
    R.div className: 'opinion_filter_region',
      R.a className: "filter_option #{if @data().selected_segment[0]==null then 'selected' else ''}", onClick: (=> @onSelectSegment(null)), 'show all opinions'        
      R.a className: "filter_option #{if @data().selected_segment[0]==0 then 'selected' else ''}", onClick: (=> @onSelectSegment(0)), 'only entrenched opposers'
      R.a className: "filter_option #{if @data().selected_segment[0]==1 then 'selected' else ''}", onClick: (=> @onSelectSegment(1)), 'only firm opposers'
      R.a className: "filter_option #{if @data().selected_segment[0]==2 then 'selected' else ''}", onClick: (=> @onSelectSegment(2)), 'only leaning oppose'
      R.a className: "filter_option #{if @data().selected_segment[0]==3 then 'selected' else ''}", onClick: (=> @onSelectSegment(3)), 'only neutrals'
      R.a className: "filter_option #{if @data().selected_segment[0]==4 then 'selected' else ''}", onClick: (=> @onSelectSegment(4)), 'only leaning support'
      R.a className: "filter_option #{if @data().selected_segment[0]==5 then 'selected' else ''}", onClick: (=> @onSelectSegment(5)), 'only firm supporters'
      R.a className: "filter_option #{if @data().selected_segment[0]==6 then 'selected' else ''}", onClick: (=> @onSelectSegment(6)), 'only entrenched supporters'



##
# DecisionBoard
# Handles the user's list of important points in crafting page. 
DecisionBoard = ReactiveComponent
  displayName: 'DecisionBoard'

  componentDidMount : ->
    # make this a drop target
    $el = $(@getDOMNode())
    $el.droppable
      accept: ".point_content"
      drop : (ev, ui) =>
        if ui.draggable.parent().is('.community_point')
          proposal_data = @data(active_proposal_key())
          ui.draggable.parent().velocity 'fadeOut', 200, -> 
            included_points = proposal_data.included_points.splice(0)
            included_points.push ui.draggable.parent().data('id')
            proposal_data.included_points = included_points
          $el.removeClass "user_is_hovering_on_a_drop_target"
      out : (ev, ui) => 
        $el.removeClass("user_is_hovering_on_a_drop_target") if ui.draggable.parent().is('.community_point')
      over : (ev, ui) => 
        $el.addClass("user_is_hovering_on_a_drop_target") if ui.draggable.parent().is('.community_point')

  render : ->
    sticky_opinion = @data(active_proposal_key()).sticky_opinion
    page = @data('application').page



    sticky_header = sticky_opinion && page == 'crafting'

    # nasty, reference to other sticky components' height...
    if sticky_header
      sticky_top = $('.fixed_proposal_heading').outerHeight() + 101
    ## 

    opinion_region_style = 
      position: if sticky_header then 'fixed' else ''
      top: if sticky_header then sticky_top else ''
      left: '50%'
      width: DECISION_BOARD_WIDTH
      marginLeft: -DECISION_BOARD_WIDTH / 2

    R.div className:'opinion_region', style: opinion_region_style,
      R.div 
        className:'decision_board_body'
        onClick: => if @data('application').page == 'results' then togglePage('crafting')
      ,

        # only shown during crafting, but needs to be present always for animation
        R.div className: 'your_points',
          # your pros
          YourPoints { key: 'pros' }

          # your cons
          YourPoints { key: 'cons' }

        # only shown during results, but needs to be present always for animation
        R.a 
          className:'give_opinion_button'
        , 'Give your Opinion'

      R.div 
        className:'save_opinion_button'
        onClick: (=> togglePage('results'))
      , 'Save your opinion and see results'

      R.a 
        className:'cancel_opinion_button'
        onClick: (=> togglePage('results'))
      , 'discard your opinion'



##
# YourPoints
# List of important points for the active user. 
# Two instances used for Pro and Con columns. Shown as part of DecisionBoard. 
# Creates NewPoint instances.
YourPoints = ReactiveComponent
  displayName: 'YourPoints'

  render : ->
    included_points = @data(active_proposal_key()).included_points
    left_or_right = if @props.key == 'pros' then 'left' else 'right'

    R.div 
      className:"points_on_decision_board #{@props.key}_on_decision_board"
      style: { width: POINT_WIDTH }
    ,
      R.div className:'points_heading_label',
        "List Your #{capitalize(@props.key)}"

      R.ul null,
        for point_id in included_points
          is_pro = @data("point/#{point_id}").is_pro
          if is_pro == (@props.key == 'pros')
            Point { key: point_id, location_class: 'decision_board_point' }

        R.div className:'add_point_drop_target',
          R.div className: "drop_target_circle_#{left_or_right}"
          R.img className:'drop_target', src: "<%= asset_path 'drop_target.png' %>"
          R.span className:'drop_prompt',
            #"Drag #{@props.valence} points from the #{if @props.valence == 'pro' then 'left' else 'right'} that resonate with you."
            "Drag a #{capitalize(if @props.key=='pros' then 'pro' else 'con')} from the #{left_or_right}"

        NewPoint { key: @props.key }

##
# CommunityPoints
# List of points contributed by others. 
# Shown in wing during crafting, in middle on results. 
CommunityPoints = ReactiveComponent
  displayName: 'CommunityPoints'

  componentDidMount : ->
    @setPointMouseover()

  ## ##
  # On hovering over a point, highlight the people who included this 
  # point in the Histogram.
  setPointMouseover : ->
    $el = $(@getDOMNode())

    $el.on 'mouseenter mouseleave', '.includers', (ev) => 

      if @data('application').page == 'results'
        point_id = $(ev.currentTarget).parent().data('id')
        includers = @data("point/#{point_id}").includers
        proposal_data = @data(active_proposal_key())

        if ev.type == 'mouseenter'          
          proposal_data.highlighted_users = includers
        else if ev.type == 'mouseleave'
          _.delay =>
            if proposal_data.highlighted_users == includers
              proposal_data.highlighted_users = null
          , 200

  buildNewPointThreshold : ->
    # Grab the 10th percentile
    points = @data(active_proposal_key()).points
    newpoint_threshold = 
      (_.sortBy points, (pnt) => - Date.parse(pnt.created_at))[Math.ceil(points.length / 10)]

    (newpoint_threshold and Date.parse(newpoint_threshold.created_at)) or 
      new Date()

  buildPointsList : ->
    page = @data('application').page
    selected_segment = @data('opinionfilter').selected_segment
    proposal_data = @data(active_proposal_key())
    included_points = proposal_data.included_points
    points = proposal_data.points
    opinions = proposal_data.opinions

    points = 
      _.filter points, (pnt) =>
        is_correct_valence = pnt.is_pro == (@props.key == 'pros')
        has_not_been_included = page == 'results' || !_.contains(included_points, pnt.id)
        is_correct_valence && has_not_been_included
    
    if selected_segment[0]?
      # If there is a histogram segment selected, we'll have to filter down 
      # to the points that users in this segment think are important, and 
      # order them by resonance to those users. I'm doing this quite inefficiently.
      point_inclusions_per_point_for_segment = {} # map of points to including users
      _.each opinions, (opinion) =>
        if opinion.stance_segment == selected_segment[0] && opinion.point_inclusions
          for point_id in opinion.point_inclusions
            if !(point_id of point_inclusions_per_point_for_segment)
              point_inclusions_per_point_for_segment[point_id] = 1
            else
              point_inclusions_per_point_for_segment[point_id] += 1

      points = 
        _.filter points, (pnt) -> pnt.id of point_inclusions_per_point_for_segment
      points = 
        _.sortBy points, (pnt) -> -point_inclusions_per_point_for_segment[pnt.id]
    else
      # Default sort order
      points = 
        _.sortBy points, (pnt) => - if page == 'results' then pnt.score else pnt.persuasiveness

    points

  render : ->
    selected_segment = @data('opinionfilter').selected_segment
    page = @data('application').page

    #filter to pros or cons & down to points that haven't been included
    points = @buildPointsList() #todo: memoize
    newpoint_threshold = @buildNewPointThreshold() #todo: memoize
    label = capitalize @props.key

    R.div 
      className:"points_by_community #{@props.key}_by_community"
      style: { width: POINT_CONTENT_WIDTH, minHeight: jQuery(window).height() }
    , 
      R.div className:'points_heading_label', 
        if page == 'results' then "Top #{label}" else "Others' #{label}"
        R.p className:'points_segment_label', style: {visibility: if selected_segment[0] == null then 'hidden' else 'visible'},
          if selected_segment[0] == null then '-' else "for #{stance_names[selected_segment[0]]}s"          

      R.ul null, 
        if points.length > 0
          for point in points
            Point 
              key: point.id, 
              is_new: Date.parse(point.created_at) > newpoint_threshold
              location_class : 'community_point'
        else
          R.li className: 'empty_point', "No #{label} listed"

##
# Point
# A single point in a list. 
Point = ReactiveComponent
  displayName: 'Point'

  getDefaultData : ->
    show_details : false

  componentDidMount : ->
    @setShowDetails()
    @setDraggability()

  componentDidUpdate : -> 
    @setDraggability()

  setDraggability : ->
    # Ability to drag include this point if a community point, 
    # or drag remove for point on decision board
    # also: disable for results page

    disable = @data('application').page == 'results'
    $point_content = $(@getDOMNode()).find('.point_content')
    revert = 
      if @props.location_class == 'community_point' 
      then 'invalid' 
      else (valid) =>
        if !valid
          included_points = 
            _.without @data(active_proposal_key()).included_points, @props.key
          @data(active_proposal_key()).included_points = included_points
        valid

    if $point_content.hasClass "ui-draggable"
      $point_content.draggable(if disable then 'disable' else 'enable') 
    else
      $point_content.draggable
        revert: revert
        disabled: disable

  setShowDetails : ->
    $(@getDOMNode()).click (ev) => @data().show_details = !@data().show_details

  buildIncluders : -> 
    my_data = @data()
    to_remove = [my_data.user_id]

    # only show includers from this stance segment if a segment is selected
    selected_segment = @data('opinionfilter').selected_segment
    if selected_segment[0]?
      for includer in my_data.includers
        stance_segment = @data("opinion/#{includer}").stance_segment
        if stance_segment && stance_segment != selected_segment[0]
          to_remove.push(includer)

    _.difference my_data.includers, to_remove

        
  renderIncluders : ->

    if @data().includers

      includers = @buildIncluders()

      s = #includers_style
        rows: 8
        dx: 2
        dy: 5
        col_gap: 8
        side_offset: 32 + 14 + 1

      # Now we'll go through the list from back to front
      i = includers.length

      for includer in includers
        i -= 1
        curr_column = Math.floor(i / s.rows)
        side_offset = s.side_offset + curr_column*s.col_gap + i*s.dx
        top_offset = (i % s.rows) * s.dy + 4
        left_right = if @data().is_pro then 'left' else 'right'
        style = { top: top_offset }
        style[left_right] = -side_offset

        # Finally draw the guy
        Avatar
          key: includer
          tag: R.span
          className: "point_includer_avatar"
          style: style

  renderNewIndicator : ->
    if @data().includers
      side_offset = 51
      left_right = if @data().is_pro then 'left' else 'right'
      style = 
        position: 'absolute'
        color: 'rgb(255,22,3)'
        fontSize: '11px'
        top:'41px'
        backgroundColor: 'white', zIndex: 5
      style[left_right] = "#{-side_offset}"
      R.span {style: style}, '*New*'

  render : -> 
    my_data = @data()
    selected_segment = @data('opinionfilter').selected_segment
    author_opinion = @data("opinion/#{my_data.user_id}")

    R.li 
      className: "point closed_point #{@props.location_class} 
        #{if my_data.is_pro then 'pro' else 'con'}"
      'data-id':@props.key
    ,
  
      if @props.location_class == 'community_point' && @props.is_new
        @renderNewIndicator()

      R.div className:'includers', 
        if @props.location_class == 'community_point'
          @renderIncluders()


        if !selected_segment[0] || ( author_opinion && author_opinion.stance_segment == selected_segment[0] )
          Avatar tag: R.span, key: my_data.user_id, className:"point_author_avatar"

      R.div className:'point_content', style : { width: POINT_CONTENT_WIDTH },
        if @props.location_class == 'community_point'
          R.img 
            className: 'community_point_mouth'
            src: "<%= asset_path 'community_point_mouth.png' %>"

        R.div className:'point_nutshell',
          my_data.nutshell
          if my_data.text
            if my_data.show_details or 
              (my_data.nutshell.length + my_data.text.length) < 210
                R.div className: 'point_details', dangerouslySetInnerHTML:{__html: my_data.text}
            else
              R.span className: 'point_details_tease', 
                $("<span>#{my_data.text[0..210-my_data.nutshell.length]}</span>").text() + "... [more]"

        # R.a className:'open_point_link',
        #   "#{@props.comment_count} comment#{if @props.comment_count != 1 then 's' else ''}"

##
# NewPoint
# Handles adding a new point into the system. Only rendered when proposal is
# in Crafting state. Manages whether the user has clicked "add a new point". 
# If they have, show new point form. 
NewPoint = ReactiveComponent
  displayName: 'NewPoint'

  getDefaultData : ->
    # UI STATE
    edit_mode : false

  handleAddPointBegin : (ev) -> @data().edit_mode = true
  handleAddPointCancel : (ev) -> @data().edit_mode = false

  handleSubmitNewPoint : (ev) ->
    $form = $(@getDOMNode())

    current_user_id = -2 #anon user
    point =
      id : ActiveREST.getUniqueID 'point'
      nutshell : $form.find('#nutshell').val()
      text : $form.find('#text').val()
      is_pro : @props.key == 'pros'
      user_id : current_user_id
      comment_count : 0 
      includers : [current_user_id]

    proposal_data = @data active_proposal_key()
    points = proposal_data.points.splice(0)
    points.push point
    proposal_data.points = points

    # how to not push to multiple places...
    ActiveREST.save "point/#{point.id}", point
 
    included_points = proposal_data.included_points.splice(0)
    included_points.push point.id
    proposal_data.included_points = included_points

    @data().edit_mode = false

  render : ->
    #TODO: refactor HTML/CSS for new point after we get better sense of new point redesign

    R.div className:'newpoint',
      if !@data().edit_mode
        R.div className:'newpoint_prompt',
          R.span className:'qualifier', 
            'or '
          R.span className:'newpoint_bullet', dangerouslySetInnerHTML:{__html: '&bull;'}
          R.a className:'newpoint_link', 'data-action':'write-point', onClick: @handleAddPointBegin,
            "Write a new #{capitalize(if @props.key == 'pros' then 'pro' else 'con')}"
      else
        R.div className:'newpoint_form',
          R.input id:'is_pro', name: 'is_pro', type: 'hidden', value: "#{@props.key == 'pros'}"
          R.div className:'newpoint_nutshell_wrap',
            R.textarea id:'nutshell', className:'newpoint_nutshell is_counted', cols:'28', maxLength:"140", name:'nutshell', pattern:'^.{3,}', placeholder:'Summarize your point (required)', required:'required'
            R.span className: 'count', 140
          R.div className:'newpoint_description_wrap',
            R.textarea id:'text', className:'newpoint_description', cols:'28', name:'text', placeholder:'Write a longer description (optional)', required:'required'
          R.div className:'newpoint_hide_name',
            R.input className:'newpoint-anonymous', type:'checkbox', id:"hide_name-#{@props.key}", name:"hide_name-#{@props.key}"
            R.label for:"hide_name-#{@props.key}", title:'We encourage you not to hide your name from other users. Signing your point with your name lends it more weight to other participants.', 
              'Conceal your name'
          R.div className:'newpoint-submit',
            R.a className:'newpoint-cancel', onClick: @handleAddPointCancel,
              'cancel'
            R.input className:'button', action:'submit-point', type:'submit', value:'Done', onClick: @handleSubmitNewPoint

##
# Avatar
# Displays a user's avatar
# Supports straight up img src, or using the CSS-embedded b64 for each user
Avatar = ReactiveComponent
  has_multiple_instances_with_same_key: true
  displayName: 'Avatar'
  
  getDefaultData: -> 
    show_details : false

  getDefaultProps : ->
    key: -1 # defaults to anonymous user
    tag: R.img
    img_style: null #null defaults to the css-based b64 embedded images

  componentWillUpdate : -> 
    if @data().show_details
      @insertUserName()

  insertUserName : ->
    $el = $(@getDOMNode())
    pos = $el.offset()

    $name = $("<span class='hover_avatar_name'>#{@data("user/#{@props.key}").name}</span>")
    $('body').append $name   # ugly!

    $name.css 
      left: pos.left - $name.width() / 2 + $el.width()/2
      top: pos.top - $name.height()

  showDetails : (ev) -> 
    @data().show_details = true

  hideDetails : (ev) -> 
    $('.hover_avatar_name').remove()
    @data().show_details = false

  render : ->
    show_details = @data().show_details
    className = "#{@props.className} avatar"
    id = "avatar-#{@props.key}"

    if @props.img_style && @props.key != -1
      avatar_file_name = @data("user/#{@props.key}").avatar_file_name

      if avatar_file_name
        filename = "/system/avatars/#{@props.key}/#{@props.img_style}/#{avatar_file_name}"

    attrs = 
      className: className
      id: id
      'data-id': @props.key
      onMouseEnter: @showDetails
      onMouseLeave: @hideDetails

    attrs.src = filename if filename
    

    @transferPropsTo @props.tag attrs


Application = ReactiveComponent
  displayName: 'Application'

  getDefaultData : ->
    page : @props.initial_page
    route : "/#{@props.long_id}#{if @props.initial_page == 'results' then '/results' else ''}"

  render : -> 
    R.div null,
      R.div className: 'l_header'
      R.div id: 'body', style : { minWidth: PAGE_WIDTH }, 
        R.div id: 'inner_body', #style : { width: CONTENT_WIDTH },
          Proposal { key: @props.long_id }
      R.div className: 'l_footer'


# Initialize ActiveREST with some knowledge of server endpoints
# and what to do with the data. 
ActiveREST = new ActiveRESTCache 
  routes : 
    'proposal/\\w{10}' : 
      get : 
        success : (key, proposal_data) =>
          data_to_save = []

          proposal_data.users = $.parseJSON proposal_data.users
          for user in proposal_data.users
            data_to_save.push ["user/#{user.id}", user]

          for point in proposal_data.points
            point.includers = $.parseJSON point.includers
            data_to_save.push ["point/#{point.id}", point]

          for opinion in proposal_data.opinions
            opinion.point_inclusions = $.parseJSON opinion.point_inclusions
            data_to_save.push ["opinion/#{opinion.user_id}", opinion]

          #I don't like passing key outside ActiveREST...
          data_to_save.push [key, proposal_data] 

          data_to_save

      save : {}

window.ActiveREST = ActiveREST

## ########################
## Application area

##
# load users' pictures
$.get Routes.get_avatars_path(), (data) -> $('head').append data

##
# Backbone routing
# Note: not committed to backbone. Want to experiment with other routing techniques too.
top_level_component = null
Router = Backbone.Router.extend

  routes :
    ":proposal(/)": "proposal"
    ":proposal/results(/)": "results"

  proposal : (long_id, page = 'crafting') ->

    if !top_level_component
      top_level_component = 
        React.renderComponent Application({long_id : long_id, initial_page : page}), document.getElementById('content')
    else
      app_data = ActiveREST.fetch 'application'
      _.extend app_data, 
        route : "/#{long_id}#{if page == 'results' then '/results' else ''}"
        page : page
      ActiveREST.save 'application', app_data, ['route', 'page']

  results : (long_id) -> @proposal long_id, 'results'

window.app_router = new Router()

$(document).ready -> Backbone.history.start {pushState: true}

{'fraction_non_alphanumeric': 0.11393899429932783, 'fraction_numerical': 0.008317025440313111, 'mean_word_length': 2.640569946180354, 'pattern_counts': {'":': 8, '<': 33, '<?xml version=': 0, '>': 247, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '785506', 'n_tokens_mistral': 29343, 'n_tokens_neox': 27384, 'n_words': 7288}