text: // This file is part of Indico.
  // Copyright (C) 2002 - 2021 CERN
  //
  // Indico is free software; you can redistribute it and/or
  // modify it under the terms of the MIT License; see the
  // LICENSE file for more details.
  
  /* eslint-disable import/unambiguous, object-shorthand */
  
  var TimetableDefaults = {
    topMargin: 30,
    bottomMargin: 40,
    leftMargin: 55,
    rightMargin: 5,
    resolution: 1,
    menuWidth: 150,
    blockMargin: 4, // Margin used inside timetable blocks
    wholeday: 7, // # of hours of duration for a timetable event to be seen as be during the whole day.
    minContribHeight: 20, // Minimum height for a contrib displayed inside a session TODO: remove?
    layouts: {
      compact: {
        name: 'Compact',
        values: {
          pxPerHour: 150,
          pxPerSpace: 2,
          minPxPerBlock: 50,
        },
        manager: new CompactLayoutManager(),
      },
  
      proportional: {
        name: 'Proportional',
        values: {
          pxPerHour: 120, //50 when not proportional
          minPxPerBlock: 25,
        },
        manager: new ProportionalLayoutManager(),
      },
  
      poster: {name: 'Poster', manager: new PosterLayoutManager()},
  
      room: {name: 'Room', manager: new RoomLayoutManager()},
    },
    filters: {
      session: {name: $T('Session'), filter: new SessionFilter()},
      room: {name: $T('Room'), filter: new RoomFilter()},
    },
  };
  
  type(
    'TimeTable',
    ['HistoryListener'],
    {
      /*
       * Translates the keys used in the data dictionary into titles
       * displayed in the tab control
       */
  
      _draw: function(timetableDiv) {
        return $('<div/>')
          .css({width: this.width})
          .append(
            $('<div/>').css('display', 'block'),
            this.legend,
            this.header,
            timetableDiv.dom,
            this.loadingIndicator.dom
          )
          .get();
      },
  
      _getMenu: function() {
        return Html.div();
      },
  
      postDraw: function() {
        this.timetableDrawer.postDraw();
        //this.LookupTabWidget.prototype.postDraw.call(this);
      },
  
      getData: function() {
        return this.data;
      },
  
      get_elem: function(blkId) {
        return $(this.getTimetableDrawer()._blockMap[blkId]);
      },
  
      getById: function(id) {
        var info = Util.parseId(id);
        var type = info[0];
        var compositeId = '';
        info = info.slice(2);
  
        if (type == 'Conference') {
          return this.eventInfo;
        } else if (type == 'Contribution') {
          throw 'not implemented!';
        } else if (type == 'Session') {
          return this.eventInfo.sessions[info[0]];
        } else if (type == 'SessionSlot') {
          compositeId = 's' + info[0] + 'l' + info[1];
        } else {
          throw 'unrecognized id!';
        }
  
        for (var day in this.data) {
          if (this.data[day][compositeId]) {
            return this.data[day][compositeId];
          }
        }
      },
  
      setSelectedTab: function(val) {
        // same as inherited, but return deferred
        var dfr = $.Deferred();
        $('body').one('timetable_ready', function() {
          dfr.resolve();
        });
        this.JLookupTabWidget.prototype.setSelectedTab.call(this, val);
        return dfr.promise();
      },
  
      getTimetableDrawer: function() {
        return this.timetableDrawer;
      },
  
      setData: function(data, intervalData, startTime, endTime) {
        this.timetableDrawer.setData(data, startTime, endTime);
      },
  
      _createLoadingIndicator: function() {
        return Html.div('timetableLoading', $T('Updating the timetable...'));
      },
      /*
       * To be overloaded. Header content displayed above the timetable.
       */
      _getHeader: function() {
        return Html.div({});
      },
  
      /*
       * To be overloaded. Returns the small "Session legend" displayed in
       * detailed mode (only).
       */
      _getLegend: function() {
        return Html.div({});
      },
      /*
       * To be overloaded. Returns buttons to be displayed below the tabs in
       * the tab widget.
       */
      _functionButtons: function() {
        return [];
      },
  
      //To be overloaded
      redrawLegend: function() {
        return;
      },
    },
    function(data, width, wrappingElement, detailLevel, managementMode) {
      var self = this;
      this.data = data;
  
      this.enabled = true;
  
      this.processedWarnings = new WatchList();
  
      this.width = width;
      this.loadingIndicator = this._createLoadingIndicator();
      this.header = this._getHeader();
      this.legend = $('<div/>');
    }
  );
  
  type(
    'DisplayTimeTable',
    ['TimeTable'],
    {
      filterMenu: function() {
        var self = this;
  
        var filterLink = Html.a({href: '#'}, $T('Filter'));
        this.filterMenu = new TimetableFilterMenu(filterLink, self.timetableDrawer);
  
        filterLink.observeClick(function(e) {
          var pos = filterLink.getAbsolutePosition();
          self.filterMenu.open(pos.x + filterLink.dom.offsetWidth, pos.y);
          return false;
        });
  
        return Html.ul(
          {className: 'inner', style: {display: 'none'}},
          Html.li('menuConfMiddleCell', filterLink)
        );
      },
  
      printMenu: function() {
        var self = this;
  
        var printLink = Html.a({href: '#'}, $T('Printable version'));
        printLink.observeClick(function(e) {
          self.print();
        });
        return Html.ul(
          {className: 'inner', style: {display: 'none'}},
          Html.li('menuConfMiddleCell', printLink)
        );
      },
      print: function() {
        var self = this;
  
        self.timetableDrawer.setPrintableVersion(true);
        var timetableElements = translate(self.timetableDrawer.canvas.dom.childNodes, function(
          value
        ) {
          return value;
        });
  
        var goBackLink = $('<a/>')
          .prop('href', window.location.hash)
          .html($T('Go back'))
          .css('font-size', '17px');
        var separator = $('<a/>')
          .html(' | ')
          .css('fontSize', '17px');
        var printLink = $('<a/>')
          .prop('href', window.location.hash)
          .html($T('Print'))
          .css('font-size', '17px');
  
        var links = $('<span/>')
          .append(goBackLink, separator, printLink)
          .css('float', 'right');
  
        var header = $('<div/>')
          .addClass('timetableHeader clearfix')
          .append(links)
          .css({
            'padding': '0px 5px 5px 5px',
            'border-bottom': '1px solid black',
            'text-align': 'center',
            'width': self.timetableDrawer.width,
          });
        header.append(
          $('<span/>')
            .append(self._titleTemplate(self.timetableDrawer.day))
            .css('float', 'left')
        );
  
        goBackLink.click(function() {
          location.reload();
        });
  
        printLink.click(function() {
          window.print();
        });
  
        var timetableDiv = $('<div/>')
          .append(timetableElements)
          .css({'padding-top': '20px', 'position': 'relative'});
  
        $('body').html(header.add(timetableDiv));
        $('body').css('padding', '30px');
      },
  
      pdf: function() {
        if ($('html').data('static-site')) {
          window.location = build_url(Indico.Urls.Timetable.default_pdf, {event_id: this.eventInfo.id});
        } else {
          ajaxDialog({
            url: build_url(Indico.Urls.Timetable.pdf, {event_id: this.eventInfo.id}),
            title: $T.gettext('Export to PDF'),
          });
        }
      },
  
      fullScreen: function() {
        var self = this;
  
        IndicoUI.Dialogs.Util.progress($T('Switching to full screen mode...'));
        // This timeout is needed in order to give time to the progress indicator to be rendered
        setTimeout(function() {
          self.timetableDrawer.width = $(window).width() - 50; // 50 is a width offset.
  
          var header = $('<div/>')
            .addClass('timetableFullScreenHeader clearfix')
            .css('width', self.timetableDrawer.width);
          header.append(
            $('<span/>')
              .append(self._titleTemplate(self.timetableDrawer.day))
              .css('float', 'left')
          );
  
          var timetableCanvas = $('#timetable_canvas');
          $('#timetable_canvas').width('width', self.timetableDrawer.width);
          $('body').html(header);
          $('body').css('padding', '30px');
          $('.timetableFullScreenHeader').before(self._getExtraButtons());
          $('.timetableFullScreenHeader').before(self.legend);
          $('.timetableFullScreenHeader').after(timetableCanvas);
          self.timetableDrawer.redraw(self.currentDay);
  
          self._filterSetup();
          if (self.timetableDrawer.detail.get() == 'contribution') {
            var newDetailLevel =
              self.timetableDrawer.detail.get() == 'contribution' ? 'session' : 'contribution';
            self.timetableDrawer.detail.set(newDetailLevel);
            self.toggleDetailedView();
          }
        }, 50);
      },
  
      _getExtraButtons: function() {
        var self = this;
        var container = $('<div class="tabExtraButtons"/>');
        var goBackButton = {
          btn: Html.div('buttonWhite', $T('Exit Full Screen')),
          onclick: function(btnContainer) {
            location.reload();
          },
        };
        var buttons = self._functionButtons();
        buttons[2] = goBackButton;
        $.each(buttons, function(i, btnData) {
          var btn = $('<div class="buttonContainer"/>')
            .append(btnData.btn.dom || btnData.btn)
            .click(function() {
              btnData.onclick(btn);
            });
          container.append(btn);
        });
        container.children(':first').addClass('buttonContainerLeft');
        container.children(':last').addClass('buttonContainerRight');
        goBackButton.btn.getParent().dom.style.background = '#9F883B';
        return container;
      },
  
      _filterSetup: function() {
        var self = this;
        this.filter = new TimeTableFilter(this.timetableDrawer, function() {
          // When closed restore the filter button color
          self.filterButtonContainer.css('background', '');
          return true;
        });
        this.filter.draw();
      },
  
      toggleDetailedView: function() {
        var self = this;
        var detailLevel = this.timetableDrawer.detail.get();
        var newDetailLevel = detailLevel == 'contribution' ? 'session' : 'contribution';
        this.timetableDrawer.detail.set(newDetailLevel);
        var state = newDetailLevel == 'contribution';
        this.inDetailedMode = state;
        //detailsButton.btn.set(state ? "Hide details" : "Show details");
        this.detailsButton.btn.getParent().dom.style.background = state ? '#9F883B' : '';
        this._addToHistory(this.currentDay + (state ? '.detailed' : ''));
  
        var legend;
  
        //If the "Detailed view" button is clicked and "activated".
        if (this.inDetailedMode) {
          /* Draw legend or "undraw" legend (getLegend() returns an empty div)
                 when toggling for detailed view. */
          legend = this._getLegend();
          this.legend.replaceWith(legend);
  
          if (this._legendActive) {
            self._toggleLegend(true);
          } else {
            self._legendPostDraw();
          }
        } else {
          if (this._legendActive) {
            self._toggleLegend(false);
          }
          legend = $('<div/>');
          this.legend.replaceWith(legend);
        }
        this.legend = legend;
      },
  
      _functionButtons: function() {
        var self = this;
  
        this.printButton = {
          btn: Html.div('printButtonWhite', $T('Print')),
          onclick: function(btnContainer) {
            self.print();
          },
        };
  
        this.pdfButton = {
          btn: Html.div('buttonWhite', $T('PDF')),
          onclick: function(btnContainer) {
            self.pdf();
          },
        };
  
        this.fullScreenButton = {
          btn: Html.div('buttonWhite', $T('Full screen')),
          onclick: function(btnContainer) {
            self.fullScreen();
          },
        };
  
        // TODO: Needs to be implemented
        this.linkButton = Html.div('linkButtonWhite', $T('Link'));
  
        this.detailsButton = {
          btn: Html.div(
            {className: 'buttonWhite', id: 'detailsButton'},
            Html.span({}, $T('Detailed view'))
          ),
          onclick: function() {
            self.toggleDetailedView();
          },
        };
  
        this.filterButton = {
          btn: Html.div('buttonWhite', $T('Filter')),
          onclick: function(btnContainer) {
            // Save the container so that the filter button background
            // color can be restored when filter is closed
            self.filterButtonContainer = btnContainer;
            self.filter.toggle();
            var state = self.filter.state.get();
            self._filterActive = state;
            btnContainer.css('background', state ? '#9F883B' : '');
          },
        };
  
        return [
          this.printButton,
          this.pdfButton,
          this.fullScreenButton,
          this.detailsButton,
          this.filterButton,
        ];
      },
    },
    function(data, width, wrappingElement, detailLevel) {
      this.TimeTable(data, width, wrappingElement, detailLevel, false);
  
      // Set data[all] so that the All days tab is created
      if (keys(data).length > 1) {
        this.data.all = data;
      }
    }
  );
  
  type(
    'TopLevelTimeTableMixin',
    ['JLookupTabWidget'],
    {
      draw: function() {
        return this.JLookupTabWidget.prototype.draw.call(this);
      },
  
      getDays: function() {
        return this.sortedKeys;
      },
  
      disable: function() {
        this.enabled = false;
        this.JLookupTabWidget.prototype.disable.call(this);
      },
  
      enable: function() {
        this.enabled = true;
        this.JLookupTabWidget.prototype.enable.call(this);
      },
  
      _titleTemplate: function(text) {
        if (text == 'all') {
          return $T('All days');
        }
  
        return moment(text).format(
          $T.pgettext('momentjs date format for timetable tab headers', 'ddd DD/MM')
        );
      },
  
      _parseDayInterval: function(hash) {
        var m = hash.match(/#(\d{8}|all)(?:\.((?:s\d+)|detailed))?/);
  
        if (m) {
          return [m[1], m[2]];
        } else {
          return [null, null];
        }
      },
  
      switchToInterval: function(intervalId) {
        var dfr = $.Deferred();
        this.disable();
  
        var intervalInfo = this.data[this.currentDay][intervalId];
        var data = intervalInfo.entries;
  
        this.intervalTimeTable = new IntervalManagementTimeTable(
          this,
          data,
          intervalInfo,
          this.eventInfo,
          this.width.slice(0, -2),
          this.canvas[0],
          'contribution',
          this.isSessionTimetable,
          this.customLinks,
          this.canManageSession,
          this.canManageBlocks,
          this.canManageContribs
        );
  
        this.intervalTimeTable.setData(intervalInfo);
        var content = this.intervalTimeTable.draw();
        this.canvas.html(content[0]);
        this.intervalTimeTable.postDraw();
  
        $('body').trigger('timetable_switch_interval', this.intervalTimeTable);
  
        dfr.resolve();
        return dfr.promise();
      },
  
      postDraw: function() {
        this.TimeTable.prototype.postDraw.call(this);
      },
  
      switchToTopLevel: function(day) {
        day = day || this.currentDay;
        var dfr = $.Deferred();
        this.enable();
        this.setSelectedTab(day || this.currentDay);
        this._generateContent(this.getSelectedPanel());
        this.timetableDrawer.redraw();
  
        // Refresh header (menu, etc...)
        var header = this._getHeader();
        this.header.replaceWith(header);
        this.header = header;
  
        $('body').trigger('timetable_switch_toplevel', this);
        window.location = '#' + day;
  
        dfr.resolve();
        return dfr.promise();
      },
    },
    function(
      data,
      width,
      wrappingElement,
      detailLevel,
      managementActions,
      historyBroker,
      timetableLayoutId
    ) {
      var self = this;
  
      this.managementActions = managementActions;
  
      this.canvas = Html.div({});
  
      historyBroker.addListener(this);
  
      this.timetableDrawer = new TimetableDrawer(
        this,
        width,
        wrappingElement,
        detailLevel,
        this._functionButtons(),
        this.loadingIndicator,
        !!managementActions,
        managementActions,
        timetableLayoutId
      );
  
      this.sortedKeys = keys(this.data);
      this.sortedKeys.sort();
  
      var today = new Date();
      var todayStr = IndicoUtil.formatDate2(today);
  
      var originalHash = window.location.hash;
  
      var dayAndInterval = this._parseDayInterval(originalHash);
      var initialTab = null;
  
      if (dayAndInterval[0]) {
        initialTab = dayAndInterval[0];
      }
  
      // if nothing is found
      if (initialTab === null) {
        // look for today
        if (exists(data[todayStr])) {
          initialTab = todayStr;
        } else {
          // look for the first non-empty day
          for (var day in this.data) {
            if (Object.getOwnPropertyNames(this.data[day]).length > 0) {
              initialTab = day;
              break;
            }
          }
          if (initialTab === null) {
            // otherwise show the first day
            initialTab = this.sortedKeys[0];
          }
        }
      }
  
      this.currentDay = initialTab;
  
      this.JLookupTabWidget(
        translate(this.sortedKeys, function(key) {
          return [
            key,
            function() {
              var detailed = self.inDetailedMode ? '.detailed' : '';
  
              self.currentDay = key;
              // each time one tab is clicked,
              // drawDay is called over a different day
              if (key == 'all') {
                self._addToHistory('all' + detailed);
                return self._draw(self.timetableDrawer.drawAllDays());
              } else {
                // Are we switching to interval mode
                if (!self.enabled) {
                  // stop everything, we don'w want the LookupTabWidget
                  // to replace the contents
                  throw 'stopDrawing';
                }
  
                self._addToHistory(key + detailed);
                return self._draw(self.timetableDrawer.drawDay(key));
              }
            },
          ];
        }),
        this.width,
        100,
        initialTab,
        this._functionButtons(),
        this.canvas
      );
      this.makeScrollable();
  
      if (dayAndInterval[1]) {
        var subref = dayAndInterval[1];
        // TODO: replace with appropriate notification system
        setTimeout(function() {
          if (subref == 'detailed') {
            self.toggleDetailedView();
          } else {
            self.switchToInterval(subref);
          }
        }, 500);
      }
    }
  );
  
  type(
    'IntervalTimeTableMixin',
    [],
    {
      draw: function() {
        // Add hash to history
        this.parentTimetable._addToHistory(this.currentDay + '.' + this.contextInfo.id);
  
        return this._draw(this.timetableDrawer.drawDay(this.currentDay));
      },
  
      _getInfoBoxContent: function() {
        return Html.div(
          {},
          Html.span(
            {style: {fontStyle: 'italic', fontSize: '0.9em'}},
            $T('You are viewing the contents of the session block:')
          ),
          Html.div(
            {style: {fontWeight: 'bold', marginTop: '5px', fontSize: '1.3em'}},
            this._generateSlotTitle(this.contextInfo),
            Html.span(
              {style: {fontWeight: 'normal'}},
              ' (',
              $B(Html.span({}), this.slotStartTime),
              ' - ',
              $B(Html.span({}), this.slotEndTime),
              ')'
            )
          )
        );
      },
  
      postDraw: function() {
        this.timetableDrawer.postDraw();
      },
  
      setData: function(data) {
        var day = IndicoUtil.formatDate2(IndicoUtil.parseJsonDate(data.startDate));
        var ttData = {};
  
        this.data = data.entries;
  
        // WatchValues, so that interval changes can be handled
        this.slotStartTime = new WatchValue(data.startDate.time.substring(0, 5));
        this.slotEndTime = new WatchValue(data.endDate.time.substring(0, 5));
  
        ttData[day] = data.entries;
  
        this.currentDay = day;
        this.timetableDrawer.setData(ttData, day, data.isPoster);
  
        // The time must be update each time new data is set
        this.infoBox.set(this._getInfoBoxContent());
      },
  
      getDays: function() {
        return this.parentTimetable.getDays();
      },
  
      getById: function(id) {
        return this.parentTimetable.getById(id);
      },
    },
    function(parent, width, wrappingElement, managementActions, layout) {
      this.managementActions = managementActions;
      this.parentTimetable = parent;
  
      this.timetableDrawer = new IntervalTimetableDrawer(
        this,
        width,
        wrappingElement,
        this._functionButtons(),
        this.loadingIndicator,
        !!managementActions,
        managementActions,
        layout || 'compact'
      );
    }
  );
  
  type(
    'ManagementTimeTable',
    ['TimeTable', 'UndoMixin'],
    {
      _generateSlotTitle: function(slotData) {
        return slotData.title + (slotData.slotTitle ? ': ' + slotData.slotTitle : '');
      },
  
      _createInfoArea: function() {
        // this is a client-side hack that compensates some algorithm weaknesses
  
        var closeButton = Html.div({
          className: 'balloonPopupCloseButton',
          style: {position: 'absolute', top: '10px', right: '10px', padding: '0px'},
        });
  
        var self = this;
  
        closeButton.observeClick(function() {
          self._hideWarnings();
        });
  
        return Html.div(
          'timetableManagementInfoArea',
          Html.div({}, $T('Your changes triggered the automatic modification of some settings:')),
          $B(Html.ul({}), this.processedWarnings, function(item) {
            var title = item[4];
            var atoms = Util.parseId(item[1]);
  
            var message = {
              OWNER_START_DATE_EXTENDED: {
                SessionSlot:
                  $T('The <strong>starting time</strong> of the session interval <strong>') +
                  title +
                  $T('</strong> was moved from '),
                Session:
                  $T('The <strong>starting time</strong> of the session interval <strong>') +
                  title +
                  $T('</strong> was moved from '),
                Conference: $T(
                  'The <strong>starting time</strong> of the <strong>Event</strong> was moved from '
                ),
              },
              OWNER_END_DATE_EXTENDED: {
                SessionSlot:
                  $T('The <strong>ending time</strong> of the session interval <strong>') +
                  title +
                  $T('</strong> was moved from '),
                Session:
                  $T('The <strong>ending time</strong> of the session interval <strong>') +
                  title +
                  $T('</strong> was moved from '),
                Conference: $T(
                  'The <strong>ending time</strong> of the <strong>Event</strong> was moved from '
                ),
              },
              ENTRIES_MOVED: {
                SessionSlot:
                  $T('The contents of the interval <strong>') +
                  title +
                  $T('</strong> were moved from '),
              },
            }[item[0]][atoms[0]];
  
            var span = Html.span({style: {verticalAlign: 'middle', marginLeft: '5px'}});
            span.dom.innerHTML =
              message +
              ' <strong>' +
              item[3] +
              '</strong>' +
              $T(' to ') +
              '<strong>' +
              item[2] +
              '</strong>';
            return Html.li({}, span);
          }),
          closeButton
        );
      },
  
      _updateMovedEntry: function(result, oldEntryId) {
        return this._updateEntry(result, oldEntryId, function(data) {
          var oldDate = IndicoUtil.formatDateTime(
            IndicoUtil.parseJsonDate(result.old.startDate),
            'd/MM/YYYY hh:mm'
          );
          var oldDay = Util.formatDateTime(
            Util.parseDateTime(oldDate, IndicoDateTimeFormats.Default),
            IndicoDateTimeFormats.Ordinal
          );
          if (result.old.sessionId) {
            // block was inside session slot
            delete data[oldDay]['s' + result.old.sessionSlotEntryId].entries[result.old.id];
          } else {
            // block was in top level
            delete data[oldDay][result.old.id];
          }
  
          if (result.slotEntry) {
            // block moves inside session slot
            data[result.day][result.slotEntry.id].entries[result.id] = result.entry;
            // updates the time of the session if it has to be extended
            data[result.day][result.slotEntry.id].startDate.time = result.slotEntry.startDate.time;
            data[result.day][result.slotEntry.id].endDate.time = result.slotEntry.endDate.time;
            data[result.day][result.slotEntry.id].duration = result.slotEntry.duration;
          } else {
            // block moves to top level
            data[result.day][result.id] = result.entry;
          }
        });
      },
  
      /*
       *
       * Is called every time a timetable entry has been successfully
       * added or updated. Updates and redraws the timetable.
       * @param originalArgs this are the original args. If they are passed, we can remove the entry
       * from the index before adding it again (just in case the date has changed).
       *
       */
  
      _processAutoOps: function(result) {
        this._hideWarnings();
  
        var self = this;
  
        if (result.autoOps && result.autoOps.length > 0) {
          each(result.autoOps, function(op) {
            var warning = self._processWarning(op);
            if (warning && self.processedWarnings.indexOf(warning) === null) {
              self.warningArea.dom.style.display = 'block';
              self.processedWarnings.append(warning);
            }
          });
        }
      },
  
      _hideWarnings: function() {
        this.warningArea.dom.style.display = 'none';
        this.warnings.clear();
        this.processedWarnings.clear();
      },
  
      _processWarning: function(entry) {
        /*
         * entry - the warning 'entry', a list [src, msg, target, newValue]
         * startTime - the original starting time for the timeblock
         * endTime - the original ending time for the timeblock
         * [slotTitle] - title, if the entry is a slot
         */
  
        var msg = entry[1];
        var finalTime = entry[3];
  
        var type = Util.parseId(entry[2])[0];
  
        var conference = null;
        var slot = null;
        var title = '';
  
        var startTime = '';
        var endTime = '';
  
        if (type == 'Session') {
          return null;
        } else if (type == 'Conference') {
          conference = this.getById(entry[2]);
          title = conference.title;
          startTime = conference.startDate.time.slice(0, 5);
          endTime = conference.endDate.time.slice(0, 5);
        } else if (type == 'SessionSlot') {
          slot = this.getById(entry[2]);
          startTime = slot.startDate.time.slice(0, 5);
          endTime = slot.endDate.time.slice(0, 5);
          title = this._generateSlotTitle(slot);
        }
  
        if (msg == 'OWNER_END_DATE_EXTENDED') {
          // Make sure that something changed, otherwise the
          // warning will be supressed
          if (endTime != finalTime) {
            // slice(1) to ignore first value
            return concat(entry.slice(1), [endTime, title]);
          }
        } else if (msg == 'OWNER_START_DATE_EXTENDED') {
          // Again, make sure that something changed
  
          if (startTime != finalTime) {
            // slice(1) to ignore first value
            return concat(entry.slice(1), [startTime, title]);
          }
        } else {
          return concat(entry.slice(1), [startTime, title]);
        }
  
        return null;
      },
  
      _allowCreateHere: function(elementType) {
        switch (elementType) {
          case 'Session':
            return this.contextInfo._type == 'Conference';
          case 'Break':
            return this.contextInfo._type == 'Conference'
              ? true
              : this.contextInfo.isPoster === false;
          case 'Contribution':
            return true;
        }
      },
  
      _retrieveSessionColor: function(session) {
        return this.getById('s' + session.id).color;
      },
  
      _openSessionMenu: function(triggerElement, parent) {
        if (exists(this.addMenu) && this.addMenu.isOpen()) {
          return;
        }
  
        var self = this;
        var sessions = {};
        each(this.eventInfo.sessions, function(session, key) {
          sessions[session.id] = {};
          sessions[session.id].func = function() {
            self.managementActions.addSessionSlot(session);
          };
          sessions[session.id].color = self._retrieveSessionColor(session);
          sessions[session.id].title = session.title;
        });
  
        var menu = {
          '': {
            content: {
              'Create a new session': function() {
                self.managementActions.addSession();
                $('.button-menu').dropdown('close');
              },
            },
            description: '',
          },
          'Add another block to:': {content: sessions, description: ''},
        };
  
        var te = new Html(triggerElement.find('a').get(0));
        var sessMenu = new SessionSectionPopupMenu(
          menu,
          [te],
          'timetableSectionPopupList popupListChained',
          true,
          true
        );
  
        var pos = triggerElement.offset();
        sessMenu.open(pos.left, pos.top - 1);
      },
  
      _createAddMenu: function(elem) {
        var self = this;
        var ul = $('<ul class="i-dropdown"/>');
  
        if (this._allowCreateHere('Session')) {
          var sessionAdd = $('<a href="#"/>')
            .text($T('Session block'))
            .appendTo(ul)
            .wrap('<li/>');
          sessionAdd.bind('menu_select', function() {
            if (keys(self.eventInfo.sessions).length === 0) {
              $(this)
                .closest('.group')
                .dropdown('close');
              self.managementActions.addSession();
            } else {
              self._openSessionMenu($(this).parent(), ul);
            }
            return true;
          });
        }
  
        if (this._allowCreateHere('Contribution')) {
          $('<a href="#"/>')
            .text($T('Contribution'))
            .bind('menu_select', function() {
              self.managementActions.addContribution();
              // Dirty hack to avoid Chrome glitch
              $(this)
                .closest('.group')
                .dropdown('close');
              return false;
            })
            .appendTo(ul)
            .wrap('<li/>');
        }
        if (this._allowCreateHere('Break')) {
          $('<a href="#"/>')
            .text($T('Break'))
            .bind('menu_select', function() {
              self.managementActions.addBreak();
              $(this)
                .closest('.group')
                .dropdown('close');
              return false;
            })
            .appendTo(ul)
            .wrap('<li/>');
        }
  
        return ul;
      },
  
      _getHeader: function() {
        var self = this;
  
        this.infoBox = Html.div({className: 'toolbar f-j-end', style: {margin: '5px'}});
  
        this.addMenuLink = this.contextInfo.isPoster
          ? $('<a href="#" data-toggle="menu_select"/>')
              .text($T('Add poster'))
              .bind('menu_select', function() {
                self.managementActions.addContribution();
              })
          : $('<a href="#" id="add_new" class="arrow hide-if-locked" data-toggle="dropdown">').text(
              $T('Add new')
            );
  
        this.rescheduleLink = $('<a href="#" class="hide-if-locked" data-toggle="menu_select">').text(
          $T('Reschedule')
        );
        this.rescheduleLink.bind('menu_select', function() {
          var popup = new RescheduleDialog(self);
          popup.open();
          return false;
        });
  
        this.fitInnerTimetableLink = $(
          '<a href="#" class="hide-if-locked" data-toggle="menu_select">'
        ).text($T('Fit to content'));
        this.fitInnerTimetableLink.bind('menu_select', function() {
          var popup = new FitInnerTimetableDialog(self);
          popup.open();
          return false;
        });
  
        this.addIntervalLink = $(
          '<a href="#" class="hide-if-locked" data-toggle="menu_select"/>'
        ).text($T('Add new block'));
        this.addIntervalLink.bind('menu_select', function() {
          self.managementActions.addSessionSlot(self.eventInfo.timetableSession);
          return false;
        });
  
        var customLinks = $();
        for (var linkName in this.customLinks) {
          var link = $('<a href="#" class="hide-if-locked">')
            .text(linkName)
            .addClass('js-{0}'.format(this.customLinks[linkName]))
            .data('timetable', self);
          customLinks = customLinks.add(link);
        }
  
        this.warningArea = this._createInfoArea();
        this.warningArea.dom.style.display = 'none';
  
        this.menu = $('<div class="group"/>');
  
        if (this.isSessionTimetable && this.isTopLevel) {
          if (this.canManageBlocks) {
            this.menu.append(this.addIntervalLink);
          }
        } else {
          this.menu.append(this.addMenuLink);
        }
  
        if (!this.contextInfo.isPoster) {
          if (this.canManageBlocks) {
            if (this.contextInfo.entryType == 'Session') {
              this.fitInnerTimetableLink.appendTo(this.menu);
            }
            this.rescheduleLink.appendTo(this.menu);
          }
        }
  
        customLinks.appendTo(this.menu);
  
        var tt_hour_tip = $('<div id="tt_hour_tip"/>')
          .hide()
          .append(
            $('<img/>')
              .prop('src', imageSrc('tt_time'))
              .prop('title', $T('Add one hour'))
          );
        var tt_status_info = $('<div id="tt_status_info" />');
  
        this.menu.children('a').addClass('i-button');
  
        if (!this.contextInfo.isPoster && (!this.isTopLevel || !this.sessionTimetable)) {
          this.menu.find('#add_new').after(this._createAddMenu(this.addMenuLink.parent()));
        }
  
        var ret = $('<div/>').append(
          this.warningArea.dom,
          $('<div id="headPanel" class="ui-follow-scroll"></div>').append(
            $('<div class="toolbar f-j-end" id="tt_menu"/>').append(
              this.menu.dropdown({effect_on: 'slideDown'}),
              tt_status_info
            )
          ),
          this.infoBox.dom,
          tt_hour_tip
        );
  
        var extra = this.getTTMenu();
        if (extra) {
          ret.find('#tt_menu .group').after(extra);
        }
  
        return ret;
      },
    },
    function(
      data,
      contextInfo,
      eventInfo,
      width,
      wrappingElement,
      detailLevel,
      customLinks,
      canManageSession,
      canManageBlocks,
      canManageContribs,
      canManageEvent
    ) {
      this.customLinks = customLinks;
      this.eventInfo = eventInfo;
      this.contextInfo = contextInfo;
      this.canManageSession = canManageSession || false;
      this.canManageBlocks = canManageBlocks || false;
      this.canManageContribs = canManageContribs || false;
      this.canManageEvent = canManageEvent || false;
      this.warnings = new WatchList();
      this.TimeTable(data, width, wrappingElement, detailLevel, true);
    }
  );
  
  type(
    'TopLevelDisplayTimeTable',
    ['DisplayTimeTable', 'TopLevelTimeTableMixin'],
    {
      _retrieveHistoryState: function(hash) {
        var currentDay = this._parseDayInterval(hash)[0];
        this.setSelectedTab(currentDay);
      },
  
      _getLegend: function() {
        var self = this;
        //Initially show N and have the rest hidden (buried under "...more")
        self._maxLegendItemsShownInitially = 4;
  
        var toggleLegendButton = $('<div id="legendMainToggle">' + $T('Session legend') + '</div>');
  
        if (this._legendActive) {
          toggleLegendButton.addClass('active');
        }
  
        toggleLegendButton.click(function() {
          self._legendActive = !self._legendActive;
          self._toggleLegend(self._legendActive);
        });
  
        self._toggleLegendButton = toggleLegendButton;
  
        this.legendItems = self._legendItemsContainer();
        // hide it so that we can execute the nice scroll down animation
        this.legendItems.hide();
  
        // create 'update' event that will be called when the day changes (redraw)
        this.legendItems.bind('update', function() {
          $(this).html(self._legendItemsContainer().children());
          if (self._legendActive && self.legendSessionInfo[self.currentDay].length) {
            $(this).slideDown();
          } else {
            $(this).slideUp();
          }
          self._legendPostDraw();
        });
  
        return $('<div id="wholeLegend"/>')
          .append(toggleLegendButton)
          .append(this.legendItems);
      },
  
      _legendItemsContainer: function() {
        var self = this;
        var state = false;
        var moreText = $T('see more...');
        var showMoreLink = $('<a id="showMoreLink" class="showMoreLink">' + moreText + '</a>').click(
          function() {
            if (!state) {
              self._fadeShowAllLegendItems();
              showMoreLink.text($T('less...'));
            } else {
              self._fadeHideLegendItems();
              showMoreLink.text(moreText);
            }
            self._legendPostDraw();
            state = !state;
          }
        );
  
        var closeButton = $('<div class="legendCloseButton"/>').click(function() {
          self._toggleLegend(false);
          self._legendActive = false;
        });
  
        var sessions = self.legendSessionInfo[this.currentDay];
        var container = $(
          '<div id="timeTableLegend" class="timeTableLegend ui-follow-scroll">'
        ).append(closeButton);
  
        if (sessions.length) {
          // Returns a div with each color + session name element
          var legendElements = self._generateLegendDivItems(sessions);
          container.append($('<div id="legendItemContainer"/>').append(legendElements));
          if (sessions.length > self._maxLegendItemsShownInitially) {
            container.append(showMoreLink);
          }
        }
        return container;
      },
  
      // Generates the "legend items"
      // (a small colored rounded square with the sessions title on its right hand side)
      // Returns a Div with those legend element items.
      _generateLegendDivItems: function(sessions) {
        var self = this;
        var showNumSessionsCounter = 0;
        var container = $('<div>');
  
        $.each(sessions, function(idx, l) {
          var div = $('<div class="legendItem" />').append(
            $('<div class="timeTableItemColour" />').css('background', l[2]),
            $('<span/>').text(l[1])
          );
  
          container.append(div);
  
          if (idx >= self._maxLegendItemsShownInitially) {
            div.hide();
          }
        });
        return container;
      },
  
      redrawLegend: function() {
        if (this.legendItems) {
          this.legendItems.trigger('update');
        }
      },
  
      // Used by the "...more"-button when you want to show more elements
      _fadeShowAllLegendItems: function() {
        $('.legendItem').fadeIn();
      },
  
      // Used by the "...less"-button when you want to hide more elementsmaxLegendItemsShownInitially
      // Note, it does NOT HIDE ALL: depending on "maxLegendItemsShownInitially".
      _fadeHideLegendItems: function() {
        $('.legendItem')
          .slice(this._maxLegendItemsShownInitially)
          .fadeOut();
      },
  
      /* This function is also called in "this.filterButton" in order
         to hide it (the Session Legend) when the Filter is brought up */
      _toggleLegend: function(state) {
        var self = this;
  
        if (state) {
          $('#legendMainToggle').addClass('active');
          if (this.legendSessionInfo[this.currentDay].length) {
            $('#timeTableLegend').slideDown();
          }
        } else {
          $('#timeTableLegend').slideUp();
          $('#legendMainToggle').removeClass('active');
        }
        this._legendPostDraw();
      },
  
      _legendPostDraw: function() {
        if ($('#detailsButton').length) {
          $('#legendMainToggle').position({
            my: 'left top',
            at: 'left bottom',
            of: $('#detailsButton').parent('.buttonContainer'),
          });
          $('#timeTableLegend:visible').width($('#timeTableLegend').get(0).clientWidth - 10);
        }
      },
  
      _extractSessionInfo: function(data) {
        // get an a dictionary where the keys are days and the values are lists
        // of [id, title, color] tuples (sessions only)
        var days = {};
  
        _(data).each(function(entries, day) {
          days[day] = _(entries)
            .chain()
            .select(function(e) {
              return e.entryType == 'Session';
            })
            .groupBy(function(e) {
              return e.sessionId;
            })
            .reduce(function(l, s) {
              return l.concat(s[0]);
            }, [])
            .map(function(e) {
              return [e.id, e.title, e.color, e.sessionId];
            })
            .sortBy(function(e) {
              return e[1];
            })
            .value();
        });
  
        // for "all days", put it all together
        days['all'] = _(days)
          .chain()
          .flatten(true)
          .groupBy(function(e) {
            return e[3];
          })
          .reduce(function(l, s) {
            return l.concat([s[0]]);
          }, [])
          .sortBy(function(e) {
            return e[1];
          })
          .value();
  
        return days;
      },
    },
    function(
      data,
      contextInfo,
      width,
      wrappingElement,
      detailLevel,
      historyBroker,
      timetableLayoutId
    ) {
      this.postDraw = TopLevelTimeTableMixin.prototype.postDraw;
      this.legendSessionInfo = this._extractSessionInfo(data);
      this._legendActive = true;
  
      this.DisplayTimeTable(data, width, wrappingElement, detailLevel);
      this.TopLevelTimeTableMixin(
        data,
        width,
        wrappingElement,
        detailLevel,
        null,
        historyBroker,
        timetableLayoutId
      );
  
      this.eventInfo = contextInfo;
      this._filterSetup();
    }
  );
  
  type(
    'TopLevelManagementTimeTable',
    ['ManagementTimeTable', 'TopLevelTimeTableMixin'],
    {
      _deleteOldEntry: function(data, result, oldEntryId) {
        /*
         * Deletes the old version of the entry
         */
  
        var oldStartDate;
        // Check whether we're operating *over* an existing slot or not
        // it is a slot && slot exists in timetable
        if (result.entry.entryType == 'Session' && data[this.currentDay][result.id]) {
          var slot = data[this.currentDay][result.id];
          oldStartDate = slot.startDate.date.replace(/-/g, '');
        } else {
          // Contribution or break - event timetable limits
          oldStartDate = this.currentDay;
        }
  
        delete data[oldStartDate][oldEntryId];
        return data[oldStartDate][oldEntryId] ? data[oldStartDate][oldEntryId].entries : null;
      },
  
      _updateEntry: function(result, oldEntryId, updateCycle) {
        var self = this;
        var data = this.getData();
  
        // AutoOp Warnings (before updates are done)
        this._processAutoOps(result);
  
        // Deletes the old version of the entry and temporarily
        // stores the entry contents to be used later on this function
        var oldContent = this._deleteOldEntry(data, result, oldEntryId);
  
        // Here's the update cycle
        if (updateCycle) {
          updateCycle(data);
        } else {
          // If none is defined in the function args,
          // execute the default action
          if (exists(result.slotEntry)) {
            data[result.day][result.slotEntry.id].entries[result.entry.id] = result.entry;
          } else {
            data[result.day][result.id] = result.entry;
          }
  
          // A session interval may contain entries, that
          // should be preserved (e.g. content (contribs, breaks) of
          // the slot).
          if (oldContent) {
            data[result.day][result.id].entries =
              result.entry.entryType == 'Session' ? result.entry.entries : oldContent;
          }
  
          // If a session slot is added, let's update the list of sessions
          if (result.session) {
            this.eventInfo.sessions[result.session.id] = result.session;
          }
        }
  
        var dfr = $.Deferred();
        $('body').one('timetable_redraw', function() {
          $('body').trigger('timetable_update', self);
          dfr.resolve();
        });
        this.timetableDrawer.redraw();
        return dfr.promise();
      },
  
      /**
       * updateDay should be used when all the entries for a given day will be changed by the
       * info received in 'update'.
       *
       * update must contain:
       *     update.day: string with a given day, e.g. "20100828"
       *     update.entries: all entries for a given day
       *     update.session: info about the session
       */
      _updateDay: function(update) {
        this._processAutoOps(update);
  
        var data = this.getData();
        var entries = {};
  
        entries[update.day] = update.entries;
        extend(data, entries);
        this._removeMissingEntries(update.entries, update.day);
  
        if (exists(update.session)) {
          this.eventInfo.sessions[update.session.id] = update.session;
        }
  
        var self = this;
        var dfr = $.Deferred();
        $('body').bind('timetable_redraw', function() {
          dfr.resolve();
          $('body').trigger('timetable_update', self);
        });
        this.timetableDrawer.redraw();
        return dfr.promise();
      },
  
      _removeMissingEntries: function(entries, day) {
        var self = this;
        _.each(self.data[day], function(value, key) {
          if (!entries[key]) {
            delete self.data[key];
          }
        });
      },
  
      _updateSessionData: function(sessionId, fields, newValues) {
        var data = this.getData();
  
        for (var day in data) {
          for (var entry in data[day]) {
            if (
              data[day][entry]['entryType'] == 'Session' &&
              data[day][entry]['sessionId'] == sessionId
            ) {
              for (var i = 0; i < fields.length; ++i) {
                data[day][entry][fields[i]] = newValues[i];
              }
            }
          }
        }
  
        this.timetableDrawer.redraw();
      },
  
      _getInfoBoxContent: function() {
        return '';
      },
  
      getTTMenu: function() {
        if (this.isSessionTimetable && this.canManageEvent) {
          var goBackLink = $('<a>', {
            class: 'icon-arrow-up i-button',
            href: build_url(Indico.Urls.Timetable.management, {event_id: this.eventInfo.id}),
          }).text($T.gettext('Go to event timetable'));
          return $('<div>', {class: 'group right'}).append(goBackLink);
        } else {
          return null;
        }
      },
  
      _retrieveHistoryState: function(hash) {
        var dayInterval = this._parseDayInterval(hash);
  
        var currentDay = dayInterval[0] || keys(this.data)[0];
        if (dayInterval[1]) {
          this.setSelectedTab(dayInterval[0]);
          this.switchToInterval(dayInterval[1]);
        } else {
          this.switchToTopLevel();
          this.setSelectedTab(dayInterval[0]);
        }
      },
    },
    function(
      data,
      eventInfo,
      width,
      wrappingElement,
      detailLevel,
      historyBroker,
      isSessionTimetable,
      customLinks,
      canManageSession,
      canManageBlocks,
      canManageContribs,
      canManageEvent
    ) {
      this.isSessionTimetable = isSessionTimetable;
      this.isTopLevel = true;
  
      this.ManagementTimeTable(
        data,
        eventInfo,
        eventInfo,
        width,
        wrappingElement,
        detailLevel,
        customLinks,
        canManageSession,
        canManageBlocks,
        canManageContribs,
        canManageEvent
      );
      var managementActions = new TopLevelTimeTableManagementActions(
        this,
        eventInfo,
        eventInfo,
        isSessionTimetable
      );
      this.TopLevelTimeTableMixin(
        data,
        width,
        wrappingElement,
        detailLevel,
        managementActions,
        historyBroker,
        'proportional'
      );
  
      this.postDraw = TopLevelTimeTableMixin.prototype.postDraw;
    }
  );
  
  type(
    'IntervalManagementTimeTable',
    ['ManagementTimeTable', 'IntervalTimeTableMixin'],
    {
      _updateTimes: function(newStartTime, newEndTime) {
        this.slotStartTime.set(newStartTime.slice(0, 5));
        this.slotEndTime.set(newEndTime.slice(0, 5));
      },
  
      _updateEntry: function(result, oldEntryId, updateCycle) {
        var self = this;
        var slot = this.contextInfo;
  
        // Delete the old entry
        delete this.parentTimetable.data[this.currentDay][slot.id].entries[oldEntryId];
  
        this._processAutoOps(result);
  
        // Here's the update cycle
        if (updateCycle) {
          updateCycle(this.parentTimetable.getData());
        } else {
          if (result.session) {
            // Account for "collateral damage" on sessions
            this.parentTimetable.eventInfo.sessions[result.session.id] = result.session;
          }
  
          if (exists(result.slotEntry)) {
            // Save the entries, otherwise they are lost
            result.slotEntry.entries = slot.entries;
            this.parentTimetable.data[result.day][result.slotEntry.id] = result.slotEntry;
            this.contextInfo = result.slotEntry;
          }
          this.parentTimetable.data[result.day][slot.id].entries[result.entry.id] = result.entry;
  
          // Update the times for the slot
          this._updateTimes(result.slotEntry.startDate.time, result.slotEntry.endDate.time);
        }
  
        var dfr = $.Deferred();
        $('body').one('timetable_redraw', function() {
          $('body').trigger('timetable_update', self);
          dfr.resolve();
        });
        this.timetableDrawer.redraw();
        return dfr.promise();
      },
  
      /**
       * updateDay should be used when all the entries for a given slot will be changed by the
       * info received in 'update'.
       *
       * update must contain:
       *     update.day: string with a given day, e.g. "20100828"
       *     update.entries: all entries for a given slot
       *     update.slotEntry: info about the slot
       *     update.session: info about the session
       */
      _updateDay: function(update) {
        this._processAutoOps(update);
  
        var slot = this.contextInfo;
        extend(this.data, update.entries);
        this._removeMissingEntries(update.entries);
  
        if (exists(update.session)) {
          this.parentTimetable.eventInfo.sessions[update.session.sessionId] = update.session;
        }
  
        if (exists(update.slotEntry)) {
          // Save the entries, otherwise they are lost
          update.slotEntry.entries = slot.entries;
          this.parentTimetable.data[update.day][update.slotEntry.id] = update.slotEntry;
          this.contextInfo = update.slotEntry;
  
          // Update the times for the slot
          this._updateTimes(update.slotEntry.startDate.time, update.slotEntry.endDate.time);
        }
  
        var dfr = $.Deferred();
        $('body').bind('timetable_redraw', function() {
          dfr.resolve();
        });
  
        this.timetableDrawer.redraw();
        return dfr.promise();
      },
  
      _removeMissingEntries: function(entries) {
        var self = this;
        _.each(self.data, function(value, key) {
          if (!entries[key]) {
            delete self.data[key];
          }
        });
      },
  
      getTTMenu: function() {
        var self = this;
        var goBackLink = $('<a class="icon-arrow-up i-button go_back" href="#"/>')
          .text($T('Up to timetable'))
          .click(function() {
            self.parentTimetable.switchToTopLevel();
            self._hideWarnings();
            self.session = null;
            return false;
          });
  
        return $('<div class="group right"/>').append(goBackLink);
      },
    },
    function(
      parent,
      data,
      contextInfo,
      eventInfo,
      width,
      wrappingElement,
      detailLevel,
      isSessionTimetable,
      customLinks,
      canManageSession,
      canManageBlocks,
      canManageContribs
    ) {
      this.isSessionTimetable = isSessionTimetable;
      this.isTopLevel = false;
      this.ManagementTimeTable(
        data,
        contextInfo,
        eventInfo,
        width,
        wrappingElement,
        detailLevel,
        customLinks,
        canManageSession,
        canManageBlocks,
        canManageContribs
      );
      var managementActions = new IntervalTimeTableManagementActions(
        this,
        eventInfo,
        contextInfo,
        isSessionTimetable
      );
      this.IntervalTimeTableMixin(parent, width, wrappingElement, managementActions, 'proportional');
  
      this.canvas = Html.div({});
      this.isPoster = contextInfo.isPoster;
  
      this.setData = IntervalTimeTableMixin.prototype.setData;
      this.getById = IntervalTimeTableMixin.prototype.getById;
      this.postDraw = IntervalTimeTableMixin.prototype.postDraw;
    }
  );
  
  type(
    'SessionDisplayTimeTable',
    ['TopLevelDisplayTimeTable'],
    {
      _functionButtons: function() {
        var self = this;
  
        this.TopLevelDisplayTimeTable.prototype._functionButtons.call(this);
  
        return [this.printButton, this.fullScreenButton, this.filterButton];
      },
    },
    function(data, eventInfo, width, wrappingElement, historyBroker) {
      this.TopLevelDisplayTimeTable(
        data,
        eventInfo,
        width,
        wrappingElement,
        'contribution',
        historyBroker,
        null
      );
    }
  );
  