CocoView = require 'views/core/CocoView'
Level = require 'models/Level'
LevelSession = require 'models/LevelSession'
ModelModal = require 'views/modal/ModelModal'
User = require 'models/User'
utils = require 'core/utils'

module.exports = class CampaignLevelView extends CocoView
  id: 'campaign-level-view'
  template: require 'templates/editor/campaign/campaign-level-view'

  events:
    'change .line-graph-checkbox': 'updateGraphCheckbox'
    'click .close': 'onClickClose'
    'click #reload-button': 'onClickReloadButton'
    'dblclick .recent-session': 'onDblClickRecentSession'
    'mouseenter .graph-point': 'onMouseEnterPoint'
    'mouseleave .graph-point': 'onMouseLeavePoint'
    'click .replay-button': 'onClickReplay'

  constructor: (options, @level) ->
    super(options)
    @fullLevel = new Level _id: @level.id
    @fullLevel.fetch()
    @listenToOnce @fullLevel, 'sync', => @render?()
    @levelSlug = @level.get('slug')
    @getAnalytics()

  getRenderData: ->
    c = super()
    c.level = if @fullLevel.loaded then @fullLevel else @level
    c.analytics = @analytics
    c

  afterRender: ->
    super()
    $("#input-startday").datepicker dateFormat: "yy-mm-dd"
    $("#input-endday").datepicker dateFormat: "yy-mm-dd"
    # TODO: Why does this have to be called from afterRender() instead of getRenderData()?
    @updateAnalyticsGraphs()

  updateGraphCheckbox: (e) ->
    lineID = $(e.target).data('lineid')
    checked = $(e.target).prop('checked')
    for graph in @analytics.graphs
      for line in graph.lines
        if line.lineID is lineID
          line.enabled = checked
          return @render()

  onClickClose: ->
    @$el.addClass('hidden')
    @trigger 'hidden'

  onClickReloadButton: () =>
    startDay = $('#input-startday').val()
    endDay = $('#input-endday').val()
    @getAnalytics startDay, endDay

  onDblClickRecentSession: (e) ->
    # Admin view of players' code
    return unless me.isAdmin()
    row = $(e.target).parent()
    player = new User _id: row.data 'player-id'
    session = new LevelSession _id: row.data 'session-id'
    @openModalView new ModelModal models: [session, player]

  onMouseEnterPoint: (e) ->
    pointID = $(e.target).data('pointid')
    container = @$el.find(".graph-point-info-container[data-pointid=#{pointID}]").show()
    margin = 20
    width = container.outerWidth()
    height = container.outerHeight()
    container.css('left', e.offsetX - width / 2)
    container.css('top', e.offsetY - height - margin)

  onMouseLeavePoint: (e) ->
    pointID = $(e.target).data('pointid')
    @$el.find(".graph-point-info-container[data-pointid=#{pointID}]").hide()

  onClickReplay: (e) ->
    sessionID = $(e.target).closest('tr').data 'session-id'
    url = "/play/level/#{@level.get('slug')}?session=#{sessionID}&observing=true"
    window.open url, '_blank'

  updateAnalyticsGraphData: ->
    # console.log 'updateAnalyticsGraphData'
    # Build graphs based on available @analytics data
    # Currently only one graph
    @analytics.graphs = [graphID: 'level-completions', lines: []]

    # TODO: Where should this metadata live?
    # TODO: lineIDs assumed to be unique across graphs
    completionLineID = 'level-completions'
    playtimeLineID = 'level-playtime'
    helpsLineID = 'helps-clicked'
    videosLineID = 'help-videos'
    lineMetadata = {}
    lineMetadata[completionLineID] =
      description: 'Level Completion (%)'
      color: 'red'
    lineMetadata[playtimeLineID] =
      description: 'Average Playtime (s)'
      color: 'green'
    lineMetadata[helpsLineID] =
      description: 'Help click rate (%)'
      color: 'blue'
    lineMetadata[videosLineID] =
      description: 'Help video rate (%)'
      color: 'purple'

    # Use this days aggregate to fill in missing days from the analytics data
    days = {}
    days["#{day.created[0..3]}-#{day.created[4..5]}-#{day.created[6..7]}"] = true for day in @analytics.levelCompletions.data if @analytics?.levelCompletions?.data?
    days[day.created] = true for day in @analytics.levelPlaytimes.data if @analytics?.levelPlaytimes?.data?
    days["#{day.day[0..3]}-#{day.day[4..5]}-#{day.day[6..7]}"] = true for day in @analytics.levelHelps.data if @analytics?.levelHelps?.data?
    days = Object.keys(days).sort (a, b) -> if a < b then -1 else 1
    if days.length > 0
      currentIndex = 0
      currentDay = days[currentIndex]
      currentDate = new Date(currentDay + "T00:00:00.000Z")
      lastDay = days[days.length - 1]
      while currentDay isnt lastDay
        days.splice currentIndex, 0, currentDay if days[currentIndex] isnt currentDay
        currentIndex++
        currentDate.setUTCDate(currentDate.getUTCDate() + 1)
        currentDay = currentDate.toISOString().substr(0, 10)

    # Update level completion graph data
    dayStartedMap = {}
    if @analytics?.levelCompletions?.data?.length > 0
      # Build line data
      levelPoints = []
      for day, i in @analytics.levelCompletions.data
        dayStartedMap[day.created] = day.started
        rate = parseFloat(day.rate)
        levelPoints.push
          x: i
          y: rate
          started: day.started
          day: "#{day.created[0..3]}-#{day.created[4..5]}-#{day.created[6..7]}"
          pointID: "#{completionLineID}#{i}"
          values: ["Started: #{day.started}", "Finished: #{day.finished}", "Completion rate: #{rate.toFixed(2)}%"]
      # Ensure points for each day
      for day, i in days
        if levelPoints.length <= i or levelPoints[i].day isnt day
          levelPoints.splice i, 0,
            y: 0.0
            day: day
            values: []
        levelPoints[i].x = i
        levelPoints[i].pointID = "#{completionLineID}#{i}"
      @analytics.graphs[0].lines.push
        lineID: completionLineID
        enabled: true
        points: levelPoints
        description: lineMetadata[completionLineID].description
        lineColor: lineMetadata[completionLineID].color
        min: 0
        max: 100.0

    # Update average playtime graph data
    if @analytics?.levelPlaytimes?.data?.length > 0
      # Build line data
      playtimePoints = []
      for day, i in @analytics.levelPlaytimes.data
        avg = parseFloat(day.average)
        playtimePoints.push
          x: i
          y: avg
          day: day.created
          pointID: "#{playtimeLineID}#{i}"
          values: ["Average playtime: #{avg.toFixed(2)}s"]
      # Ensure points for each day
      for day, i in days
        if playtimePoints.length <= i or playtimePoints[i].day isnt day
          playtimePoints.splice i, 0,
            y: 0.0
            day: day
            values: []
        playtimePoints[i].x = i
        playtimePoints[i].pointID = "#{playtimeLineID}#{i}"
      @analytics.graphs[0].lines.push
        lineID: playtimeLineID
        enabled: true
        points: playtimePoints
        description: lineMetadata[playtimeLineID].description
        lineColor: lineMetadata[playtimeLineID].color
        min: 0
        max: d3.max(playtimePoints, (d) -> d.y)

    # Update help graph data
    if @analytics?.levelHelps?.data?.length > 0
      # Build line data
      helpPoints = []
      videoPoints = []
      for day, i in @analytics.levelHelps.data
        helpCount = day.alertHelps + day.paletteHelps
        started = dayStartedMap[day.day] ? 0
        clickRate = if started > 0 then helpCount / started * 100 else 0
        videoRate = day.videoStarts / helpCount * 100
        helpPoints.push
          x: i
          y: clickRate
          day: "#{day.day[0..3]}-#{day.day[4..5]}-#{day.day[6..7]}"
          pointID: "#{helpsLineID}#{i}"
          values: ["Helps clicked: #{helpCount}", "Helps click clickRate: #{clickRate.toFixed(2)}%"]
        videoPoints.push
          x: i
          y: videoRate
          day: "#{day.day[0..3]}-#{day.day[4..5]}-#{day.day[6..7]}"
          pointID: "#{videosLineID}#{i}"
          values: ["Help videos started: #{day.videoStarts}", "Help videos start rate: #{videoRate.toFixed(2)}%"]
      # Ensure points for each day
      for day, i in days
        if helpPoints.length <= i or helpPoints[i].day isnt day
          helpPoints.splice i, 0,
            y: 0.0
            day: day
            values: []
        helpPoints[i].x = i
        helpPoints[i].pointID = "#{helpsLineID}#{i}"
        if videoPoints.length <= i or videoPoints[i].day isnt day
          videoPoints.splice i, 0,
            y: 0.0
            day: day
            values: []
        videoPoints[i].x = i
        videoPoints[i].pointID = "#{videosLineID}#{i}"
      if d3.max(helpPoints, (d) -> d.y) > 0
        @analytics.graphs[0].lines.push
          lineID: helpsLineID
          enabled: true
          points: helpPoints
          description: lineMetadata[helpsLineID].description
          lineColor: lineMetadata[helpsLineID].color
          min: 0
          max: 100.0
      if d3.max(videoPoints, (d) -> d.y) > 0
        @analytics.graphs[0].lines.push
          lineID: videosLineID
          enabled: true
          points: videoPoints
          description: lineMetadata[videosLineID].description
          lineColor: lineMetadata[videosLineID].color
          min: 0
          max: 100.0

  updateAnalyticsGraphs: ->
    # Build d3 graphs
    return unless @analytics?.graphs?.length > 0
    containerSelector = '.line-graph-container'
    # console.log 'updateAnalyticsGraphs', containerSelector, @analytics.graphs

    margin = 20
    keyHeight = 20
    xAxisHeight = 20
    yAxisWidth = 40
    containerWidth = $(containerSelector).width()
    containerHeight = $(containerSelector).height()

    for graph in @analytics.graphs
      graphLineCount = _.reduce graph.lines, ((sum, item) -> if item.enabled then sum + 1 else sum), 0
      svg = d3.select(containerSelector).append("svg")
        .attr("width", containerWidth)
        .attr("height", containerHeight)
      width = containerWidth - margin * 2 - yAxisWidth * graphLineCount
      height = containerHeight - margin * 2 - xAxisHeight - keyHeight * graphLineCount
      currentLine = 0
      for line in graph.lines
        continue unless line.enabled
        xRange = d3.scale.linear().range([0, width]).domain([d3.min(line.points, (d) -> d.x), d3.max(line.points, (d) -> d.x)])
        yRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])

        # x-Axis and guideline once
        if currentLine is 0
          startDay = new Date(line.points[0].day)
          endDay = new Date(line.points[line.points.length - 1].day)
          xAxisRange = d3.time.scale()
            .domain([startDay, endDay])
            .range([0, width])
          xAxis = d3.svg.axis()
            .scale(xAxisRange)
          svg.append("g")
            .attr("class", "x axis")
            .call(xAxis)
            .selectAll("text")
            .attr("dy", ".35em")
            .attr("transform", "translate(" + (margin + yAxisWidth * (graphLineCount - 1)) + "," + (height + margin) + ")")
            .style("text-anchor", "start")

          # Horizontal guidelines
          svg.selectAll(".line")
            .data([10, 30, 50, 70, 90])
            .enter()
            .append("line")
            .attr("x1", margin + yAxisWidth * graphLineCount)
            .attr("y1", (d) -> margin + yRange(d))
            .attr("x2", margin + yAxisWidth * graphLineCount + width)
            .attr("y2", (d) -> margin + yRange(d))
            .attr("stroke", line.lineColor)
            .style("opacity", "0.5")

        # y-Axis
        yAxisRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
        yAxis = d3.svg.axis()
          .scale(yRange)
          .orient("left")
        svg.append("g")
          .attr("class", "y axis")
          .attr("transform", "translate(" + (margin + yAxisWidth * currentLine) + "," + margin + ")")
          .style("color", line.lineColor)
          .call(yAxis)
          .selectAll("text")
          .attr("y", 0)
          .attr("x", 0)
          .attr("fill", line.lineColor)
          .style("text-anchor", "start")

        # Key
        svg.append("line")
          .attr("x1", margin)
          .attr("y1", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
          .attr("x2", margin + 40)
          .attr("y2", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
          .attr("stroke", line.lineColor)
          .attr("class", "key-line")
        svg.append("text")
          .attr("x", margin + 40 + 10)
          .attr("y", margin + height + xAxisHeight + keyHeight * currentLine + (keyHeight + 10) / 2)
          .attr("fill", line.lineColor)
          .attr("class", "key-text")
          .text(line.description)

        # Path and points
        svg.selectAll(".circle")
          .data(line.points)
          .enter()
          .append("circle")
          .attr("transform", "translate(" + (margin + yAxisWidth * graphLineCount) + "," + margin + ")")
          .attr("cx", (d) -> xRange(d.x))
          .attr("cy", (d) -> yRange(d.y))
          .attr("r", (d) -> if d.started then Math.max(3, Math.min(10, Math.log(parseInt(d.started)))) + 2 else 6)
          .attr("fill", line.lineColor)
          .attr("stroke-width", 1)
          .attr("class", "graph-point")
          .attr("data-pointid", (d) -> "#{line.lineID}#{d.x}")
        d3line = d3.svg.line()
          .x((d) -> xRange(d.x))
          .y((d) -> yRange(d.y))
          .interpolate("linear")
        svg.append("path")
          .attr("d", d3line(line.points))
          .attr("transform", "translate(" + (margin + yAxisWidth * graphLineCount) + "," + margin + ")")
          .style("stroke-width", 1)
          .style("stroke", line.lineColor)
          .style("fill", "none")
        currentLine++

  getAnalytics: (startDay, endDay) =>
    # Analytics APIs use 2 different day formats
    if startDay?
      startDayDashed = startDay
      startDay = startDay.replace(/-/g, '')
    else
      startDay = utils.getUTCDay -14
      startDayDashed = "#{startDay[0..3]}-#{startDay[4..5]}-#{startDay[6..7]}"
    if endDay?
      endDayDashed = endDay
      endDay = endDay.replace(/-/g, '')
    else
      endDay = utils.getUTCDay -1
      endDayDashed = "#{endDay[0..3]}-#{endDay[4..5]}-#{endDay[6..7]}"

    # Initialize
    @analytics =
      startDay: startDayDashed
      endDay: endDayDashed
      commonProblems: {data: [], loading: true}
      levelCompletions: {data: [], loading: true}
      levelHelps: {data: [], loading: true}
      levelPlaytimes: {data: [], loading: true}
      recentSessions: {data: [], loading: true}
      graphs: []
    @render() # Hide old analytics data while we fetch new data

    makeFinishDataFetch = (data) =>
      return =>
        return if @destroyed
        @updateAnalyticsGraphData()
        data.loading = false
        @render()
    @getCommonLevelProblems startDayDashed, endDayDashed, makeFinishDataFetch(@analytics.commonProblems)
    @getLevelCompletions startDay, endDay, makeFinishDataFetch(@analytics.levelCompletions)
    @getLevelHelps startDay, endDay, makeFinishDataFetch(@analytics.levelHelps)
    @getLevelPlaytimes startDayDashed, endDayDashed, makeFinishDataFetch(@analytics.levelPlaytimes)
    @getRecentSessions makeFinishDataFetch(@analytics.recentSessions)

  getCommonLevelProblems: (startDay, endDay, doneCallback) ->
    success = (data) =>
      return doneCallback() if @destroyed
      # console.log 'getCommonLevelProblems', data
      @analytics.commonProblems.data = data
      doneCallback()
    request = @supermodel.addRequestResource 'common_problems', {
      url: '/db/user.code.problem/-/common_problems'
      data: {startDay: startDay, endDay: endDay, slug: @levelSlug}
      method: 'POST'
      success: success
    }, 0
    request.load()

  getLevelCompletions: (startDay, endDay, doneCallback) ->
    success = (data) =>
      return doneCallback() if @destroyed
      # console.log 'getLevelCompletions', data
      data.sort (a, b) -> if a.created < b.created then -1 else 1
      mapFn = (item) ->
        item.rate = if item.started > 0 then item.finished / item.started * 100 else 0
        item
      @analytics.levelCompletions.data = _.map data, mapFn, @
      doneCallback()
    request = @supermodel.addRequestResource 'level_completions', {
      url: '/db/analytics_perday/-/level_completions'
      data: {startDay: startDay, endDay: endDay, slug: @levelSlug}
      method: 'POST'
      success: success
    }, 0
    request.load()

  getLevelHelps: (startDay, endDay, doneCallback) ->
    success = (data) =>
      return doneCallback() if @destroyed
      # console.log 'getLevelHelps', data
      @analytics.levelHelps.data = data.sort (a, b) -> if a.day < b.day then -1 else 1
      doneCallback()
    request = @supermodel.addRequestResource 'level_helps', {
      url: '/db/analytics_perday/-/level_helps'
      data: {startDay: startDay, endDay: endDay, slugs: [@levelSlug]}
      method: 'POST'
      success: success
    }, 0
    request.load()

  getLevelPlaytimes: (startDay, endDay, doneCallback) ->
    success = (data) =>
      return doneCallback() if @destroyed
      # console.log 'getLevelPlaytimes', data
      @analytics.levelPlaytimes.data = data.sort (a, b) -> if a.created < b.created then -1 else 1
      doneCallback()
    request = @supermodel.addRequestResource 'playtime_averages', {
      url: '/db/level/-/playtime_averages'
      data: {startDay: startDay, endDay: endDay, slugs: [@levelSlug]}
      method: 'POST'
      success: success
    }, 0
    request.load()

  getRecentSessions: (doneCallback) ->
    limit = 100
    success = (data) =>
      return doneCallback() if @destroyed
      # console.log 'getRecentSessions', data
      @analytics.recentSessions.data = data
      doneCallback()
    request = @supermodel.addRequestResource 'level_sessions_recent', {
      url: "/db/level.session/-/recent"
      data: {slug: @levelSlug, limit: limit}
      method: 'POST'
      success: success
    }, 0
    request.load()
