<commit_before>import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
import OptionsManager from './OptionsManager'
import View from './View'
import { OptionsInput, EventHandlerName, EventHandlerArgs } from './types/input-types'
import { buildLocale, organizeRawLocales, RawLocaleMap } from './datelib/locale'
import { DateEnv, DateInput } from './datelib/env'
import { DateMarker, startOfDay, diffWholeDays } from './datelib/marker'
import { createFormatter } from './datelib/formatting'
import { createDuration, DurationInput, Duration } from './datelib/duration'
import reduce from './reducers/main'
import { parseDateSpan, DateSpanInput, DateSpan, buildDateSpanApi, DateSpanApi, buildDatePointApi, DatePointApi } from './structs/date-span'
import { memoize } from './util/memoize'
import { mapHash, isPropsEqual } from './util/object'
import { DateRangeInput, DateRange } from './datelib/date-range'
import DateProfileGenerator, { DateProfile } from './DateProfileGenerator'
import { EventSourceInput, parseEventSource, EventSourceHash } from './structs/event-source'
import { EventInput, parseEvent, EventDefHash } from './structs/event'
import { CalendarState, Action } from './reducers/types'
import EventSourceApi from './api/EventSourceApi'
import EventApi from './api/EventApi'
import { createEmptyEventStore, eventTupleToStore, EventStore } from './structs/event-store'
import { processScopedUiProps, EventUiHash, EventUi } from './component/event-ui'
import { buildViewSpecs, ViewSpecHash, ViewSpec } from './structs/view-spec'
import { PluginSystem, PluginHooks, PluginDef } from './plugin-system'
import CalendarComponent from './CalendarComponent'
import { __assign } from 'tslib'
import DateComponent from './component/DateComponent'
import { PointerDragEvent } from './interactions/pointer'
import { InteractionSettingsInput, parseInteractionSettings, Interaction, interactionSettingsStore, InteractionClass } from './interactions/interaction'
import EventClicking from './interactions/EventClicking'
import EventHovering from './interactions/EventHovering'
import StandardTheme from './theme/StandardTheme'
import ComponentContext, { ComponentContextType, buildContext } from './component/ComponentContext'
import { render, h, createRef, flushToDom } from './vdom'
import { TaskRunner, DelayedRunner } from './util/runner'
import ViewApi from './ViewApi'
import { globalPlugins } from './global-plugins'
import { removeExact } from './util/array'
import { guid } from './util/misc'


export interface DateClickApi extends DatePointApi {
  dayEl: HTMLElement
  jsEvent: UIEvent
  view: View
}

export interface DateSelectionApi extends DateSpanApi {
  jsEvent: UIEvent
  view: View
}

export type DatePointTransform = (dateSpan: DateSpan, calendar: Calendar) => any
export type DateSpanTransform = (dateSpan: DateSpan, calendar: Calendar) => any

export type CalendarInteraction = { destroy() }
export type CalendarInteractionClass = { new(calendar: Calendar): CalendarInteraction }

export type OptionChangeHandler = (propValue: any, calendar: Calendar, deepEqual) => void
export type OptionChangeHandlerMap = { [propName: string]: OptionChangeHandler }

export type ResizeHandler = (force: boolean) => void


export default class Calendar {

  // global handler registry
  static on: EmitterInterface['on']
  static off: EmitterInterface['off']
  static trigger: EmitterInterface['trigger']

  on: EmitterInterface['on']
  one: EmitterInterface['one']
  off: EmitterInterface['off']
  trigger: EmitterInterface['trigger']
  triggerWith: EmitterInterface['triggerWith']
  hasHandlers: EmitterInterface['hasHandlers']

  // option-processing internals
  // TODO: make these all private
  public pluginSystem: PluginSystem
  public optionsManager: OptionsManager
  public viewSpecs: ViewSpecHash
  public dateProfileGenerators: { [viewName: string]: DateProfileGenerator }

  // derived state
  // TODO: make these all private
  private organizeRawLocales = memoize(organizeRawLocales)
  private buildDateEnv = memoize(buildDateEnv)
  private computeTitle = memoize(computeTitle)
  private buildTheme = memoize(buildTheme)
  private buildContext = memoize(buildContext)
  private buildEventUiSingleBase = memoize(buildEventUiSingleBase)
  private buildSelectionConfig = memoize(buildSelectionConfig)
  private buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqual)
  private buildEventUiBases = memoize(buildEventUiBases)
  private renderableEventStore: EventStore
  public eventUiBases: EventUiHash // needed for validation system
  public selectionConfig: EventUi // needed for validation system. doesn't need all the info EventUi provides. only validation-related
  private availableRawLocales: RawLocaleMap
  public context: ComponentContext
  public dateEnv: DateEnv
  public defaultAllDayEventDuration: Duration
  public defaultTimedEventDuration: Duration
  private resizeHandlers: ResizeHandler[] = []

  // interaction
  calendarInteractions: CalendarInteraction[]
  interactionsStore: { [componentUid: string]: Interaction[] } = {}

  state: CalendarState
  isRendering = false
  isRendered = false
  renderRunner: DelayedRunner
  actionRunner: TaskRunner<Action> // for reducer. bad name
  afterSizingTriggers: any = {}
  isViewUpdated: boolean = false
  isDatesUpdated: boolean = false
  isEventsUpdated: boolean = false
  el: HTMLElement
  componentRef = createRef<CalendarComponent>()
  view: ViewApi // public API

  get component() { return this.componentRef.current }


  constructor(el: HTMLElement, overrides?: OptionsInput) {
    this.el = el

    let optionsManager = this.optionsManager = new OptionsManager(overrides || {})
    this.pluginSystem = new PluginSystem()

    let renderRunner = this.renderRunner = new DelayedRunner(
      this.updateComponent.bind(this)
    )

    let actionRunner = this.actionRunner = new TaskRunner(
      this.runAction.bind(this),
      () => {
        this.updateDerivedState()
        renderRunner.request(optionsManager.computed.rerenderDelay)
      }
    )
    actionRunner.pause()

    // only do once. don't do in onOptionsChange. because can't remove plugins
    this.addPluginDefs(
      globalPlugins.concat(optionsManager.computed.plugins || [])
    )

    this.onOptionsChange()

    this.publiclyTrigger('_init') // for tests
    this.hydrate()
    actionRunner.resume()

    this.calendarInteractions = this.pluginSystem.hooks.calendarInteractions
      .map((calendarInteractionClass) => {
        return new calendarInteractionClass(this)
      })
  }


  addPluginDefs(pluginDefs: PluginDef[]) {
    for (let pluginDef of pluginDefs) {
      this.pluginSystem.add(pluginDef)
    }
  }


  // Public API for rendering
  // -----------------------------------------------------------------------------------------------------------------


  render() {
    if (!this.isRendering) {
      this.isRendering = true
      this.renderableEventStore = createEmptyEventStore()
      this.renderRunner.request()
      window.addEventListener('resize', this.handleWindowResize)
    } else {
      // hack for RERENDERING
      this.setOption('renderId', guid())
    }
  }


  destroy() {
    if (this.isRendering) {
      this.isRendering = false
      this.renderRunner.request()
      this.resizeRunner.clear()
      window.removeEventListener('resize', this.handleWindowResize)
    }
  }


  // Dispatcher
  // -----------------------------------------------------------------------------------------------------------------


  hydrate() {
    this.state = this.buildInitialState()

    let rawSources = this.opt('eventSources') || []
    let singleRawSource = this.opt('events')
    let sources = [] // parsed

    if (singleRawSource) {
      rawSources.unshift(singleRawSource)
    }

    for (let rawSource of rawSources) {
      let source = parseEventSource(rawSource, this)
      if (source) {
        sources.push(source)
      }
    }

    this.dispatch({ type: 'INIT' }) // pass in sources here?
    this.dispatch({ type: 'ADD_EVENT_SOURCES', sources })
    this.dispatch({
      type: 'SET_VIEW_TYPE',
      viewType: this.opt('defaultView') || this.pluginSystem.hooks.defaultView
    })
  }


  buildInitialState(): CalendarState {
    return {
      viewType: null,
      loadingLevel: 0,
      eventSourceLoadingLevel: 0,
      currentDate: this.getInitialDate(),
      dateProfile: null,
      eventSources: {},
      eventStore: createEmptyEventStore(),
      dateSelection: null,
      eventSelection: '',
      eventDrag: null,
      eventResize: null
    }
  }


  dispatch(action: Action) {
    this.actionRunner.request(action)

    // actions we know we want to render immediately. TODO: another param in dispatch instead?
    switch (action.type) {
      case 'SET_EVENT_DRAG':
      case 'SET_EVENT_RESIZE':
        this.renderRunner.drain()
    }
  }


  runAction(action: Action) {
    let oldState = this.state
    let newState = this.state = reduce(this.state, action, this)

    if (!oldState.loadingLevel && newState.loadingLevel) {
      this.publiclyTrigger('loading', [ true ])
    } else if (oldState.loadingLevel && !newState.loadingLevel) {
      this.publiclyTrigger('loading', [ false ])
    }

    let calendarComponent = this.component
    let viewComponent = calendarComponent && calendarComponent.view
    let viewApi = this.view // bad name

    if (oldState.eventStore !== newState.eventStore) {
      if (oldState.eventStore) {
        this.isEventsUpdated = true
      }
    }

    if (oldState.dateProfile !== newState.dateProfile) {
      if (oldState.dateProfile && viewComponent) { // why would view be null!?
        this.publiclyTrigger('datesDestroy', [
          {
            view: viewApi,
            el: viewComponent.base as HTMLElement
          }
        ])
      }
      this.isDatesUpdated = true
    }

    if (oldState.viewType !== newState.viewType) {
      if (oldState.viewType && viewComponent) { // why would view be null!?
        this.publiclyTrigger('viewSkeletonDestroy', [
          {
            view: viewApi,
            el: viewComponent.base as HTMLElement
          }
        ])
      }
      this.isViewUpdated = true
    }
  }


  // Rendering
  // -----------------------------------------------------------------------------------------------------------------


  batchRendering(func) {
    this.renderRunner.whilePaused(func)
  }


  updateComponent() {
    if (this.isRendering) {
      this.renderComponent()
      this.isRendered = true
    } else {
      if (this.isRendered) {
        this.destroyComponent()
        this.isRendered = false
      }
    }
  }


  renderComponent() {
    let { context, state } = this
    let { viewType } = state
    let viewSpec = this.viewSpecs[viewType]
    let viewApi = context.view

    // if event sources are still loading and progressive rendering hasn't been enabled,
    // keep rendering the last fully loaded set of events
    let renderableEventStore = this.renderableEventStore =
      (state.eventSourceLoadingLevel && !this.opt('progressiveEventRendering')) ?
        this.renderableEventStore :
        state.eventStore

    let eventUiSingleBase = this.buildEventUiSingleBase(viewSpec.options)
    let eventUiBySource = this.buildEventUiBySource(state.eventSources)
    let eventUiBases = this.eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource)

    render(
      <ComponentContextType.Provider value={context}>
        <CalendarComponent
          ref={this.componentRef}
          rootEl={this.el}
          { ...state }
          viewSpec={viewSpec}
          dateProfileGenerator={this.dateProfileGenerators[viewType]}
          dateProfile={state.dateProfile}
          eventStore={renderableEventStore}
          eventUiBases={eventUiBases}
          dateSelection={state.dateSelection}
          eventSelection={state.eventSelection}
          eventDrag={state.eventDrag}
          eventResize={state.eventResize}
          title={viewApi.title}
          />
      </ComponentContextType.Provider>,
      this.el
    )
    flushToDom()

    let calendarComponent = this.component
    let viewComponent = calendarComponent.view

    if (this.isViewUpdated) {
      this.isViewUpdated = false
      this.publiclyTrigger('viewSkeletonRender', [
        {
          view: viewApi,
          el: viewComponent.base as HTMLElement
        }
      ])
    }

    if (this.isDatesUpdated) {
      this.isDatesUpdated = false
      this.publiclyTrigger('datesRender', [
        {
          view: viewApi,
          el: viewComponent.base as HTMLElement
        }
      ])
    }

    if (this.isEventsUpdated) {
      this.isEventsUpdated = false
    }

    this.releaseAfterSizingTriggers()
  }


  destroyComponent() {
    render(null, this.el)

    for (let interaction of this.calendarInteractions) {
      interaction.destroy()
    }

    this.publiclyTrigger('_destroyed')
  }


  // Options
  // -----------------------------------------------------------------------------------------------------------------


  setOption(name: string, val) {
    this.mutateOptions({ [name]: val }, [], true)
  }


  getOption(name: string) { // getter, used externally
    return this.optionsManager.computed[name]
  }


  opt(name: string) { // getter, used internally
    return this.optionsManager.computed[name]
  }


  viewOpt(name: string) { // getter, used internally
    return this.viewSpecs[this.state.viewType].options[name]
  }

  /*
  handles option changes (like a diff)
  */
  mutateOptions(updates, removals: string[], isDynamic?: boolean, deepEqual?) {
    let changeHandlers = this.pluginSystem.hooks.optionChangeHandlers
    let normalUpdates = {}
    let specialUpdates = {}
    let oldDateEnv = this.dateEnv // do this before onOptionsChange
    let isTimeZoneDirty = false
    let isSizeDirty = false
    let anyDifficultOptions = Boolean(removals.length)

    for (let name in updates) {
      if (changeHandlers[name]) {
        specialUpdates[name] = updates[name]
      } else {
        normalUpdates[name] = updates[name]
      }
    }

    for (let name in normalUpdates) {
      if (/^(height|contentHeight|aspectRatio)$/.test(name)) {
        isSizeDirty = true
      } else if (/^(defaultDate|defaultView)$/.test(name)) {
        // can't change date this way. use gotoDate instead
      } else {
        anyDifficultOptions = true // I guess all options are "difficult" ?

        if (name === 'timeZone') {
          isTimeZoneDirty = true
        }
      }
    }

    this.optionsManager.mutate(normalUpdates, removals, isDynamic)

    if (anyDifficultOptions) {
      this.onOptionsChange()
    }

    this.batchRendering(() => {

      if (anyDifficultOptions) {

        if (isTimeZoneDirty) {
          this.dispatch({
            type: 'CHANGE_TIMEZONE',
            oldDateEnv
          })
        }

        /* HACK
        has the same effect as calling this.updateComponent()
        but recomputes the state's dateProfile
        */
        this.dispatch({
          type: 'SET_VIEW_TYPE',
          viewType: this.state.viewType
        })

      } else if (isSizeDirty) {
        this.updateSize()
      }

      // special updates
      if (deepEqual) {
        for (let name in specialUpdates) {
          changeHandlers[name](specialUpdates[name], this, deepEqual)
        }
      }

    })
  }


  /*
  rebuilds things based off of a complete set of refined options
  TODO: move all this to updateDerivedState, but hard because reducer depends on some values
  */
  onOptionsChange() {
    let pluginHooks = this.pluginSystem.hooks
    let rawOptions = this.optionsManager.computed

    let availableLocaleData = this.organizeRawLocales(rawOptions.locales)
    let dateEnv = this.buildDateEnv(rawOptions, pluginHooks, availableLocaleData)

    this.availableRawLocales = availableLocaleData.map
    this.dateEnv = dateEnv<commit_msg>dont handle sizing options differently in .setOption<commit_after>import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
import OptionsManager from './OptionsManager'
import View from './View'
import { OptionsInput, EventHandlerName, EventHandlerArgs } from './types/input-types'
import { buildLocale, organizeRawLocales, RawLocaleMap } from './datelib/locale'
import { DateEnv, DateInput } from './datelib/env'
import { DateMarker, startOfDay, diffWholeDays } from './datelib/marker'
import { createFormatter } from './datelib/formatting'
import { createDuration, DurationInput, Duration } from './datelib/duration'
import reduce from './reducers/main'
import { parseDateSpan, DateSpanInput, DateSpan, buildDateSpanApi, DateSpanApi, buildDatePointApi, DatePointApi } from './structs/date-span'
import { memoize } from './util/memoize'
import { mapHash, isPropsEqual } from './util/object'
import { DateRangeInput, DateRange } from './datelib/date-range'
import DateProfileGenerator, { DateProfile } from './DateProfileGenerator'
import { EventSourceInput, parseEventSource, EventSourceHash } from './structs/event-source'
import { EventInput, parseEvent, EventDefHash } from './structs/event'
import { CalendarState, Action } from './reducers/types'
import EventSourceApi from './api/EventSourceApi'
import EventApi from './api/EventApi'
import { createEmptyEventStore, eventTupleToStore, EventStore } from './structs/event-store'
import { processScopedUiProps, EventUiHash, EventUi } from './component/event-ui'
import { buildViewSpecs, ViewSpecHash, ViewSpec } from './structs/view-spec'
import { PluginSystem, PluginHooks, PluginDef } from './plugin-system'
import CalendarComponent from './CalendarComponent'
import { __assign } from 'tslib'
import DateComponent from './component/DateComponent'
import { PointerDragEvent } from './interactions/pointer'
import { InteractionSettingsInput, parseInteractionSettings, Interaction, interactionSettingsStore, InteractionClass } from './interactions/interaction'
import EventClicking from './interactions/EventClicking'
import EventHovering from './interactions/EventHovering'
import StandardTheme from './theme/StandardTheme'
import ComponentContext, { ComponentContextType, buildContext } from './component/ComponentContext'
import { render, h, createRef, flushToDom } from './vdom'
import { TaskRunner, DelayedRunner } from './util/runner'
import ViewApi from './ViewApi'
import { globalPlugins } from './global-plugins'
import { removeExact } from './util/array'
import { guid } from './util/misc'


export interface DateClickApi extends DatePointApi {
  dayEl: HTMLElement
  jsEvent: UIEvent
  view: View
}

export interface DateSelectionApi extends DateSpanApi {
  jsEvent: UIEvent
  view: View
}

export type DatePointTransform = (dateSpan: DateSpan, calendar: Calendar) => any
export type DateSpanTransform = (dateSpan: DateSpan, calendar: Calendar) => any

export type CalendarInteraction = { destroy() }
export type CalendarInteractionClass = { new(calendar: Calendar): CalendarInteraction }

export type OptionChangeHandler = (propValue: any, calendar: Calendar, deepEqual) => void
export type OptionChangeHandlerMap = { [propName: string]: OptionChangeHandler }

export type ResizeHandler = (force: boolean) => void


export default class Calendar {

  // global handler registry
  static on: EmitterInterface['on']
  static off: EmitterInterface['off']
  static trigger: EmitterInterface['trigger']

  on: EmitterInterface['on']
  one: EmitterInterface['one']
  off: EmitterInterface['off']
  trigger: EmitterInterface['trigger']
  triggerWith: EmitterInterface['triggerWith']
  hasHandlers: EmitterInterface['hasHandlers']

  // option-processing internals
  // TODO: make these all private
  public pluginSystem: PluginSystem
  public optionsManager: OptionsManager
  public viewSpecs: ViewSpecHash
  public dateProfileGenerators: { [viewName: string]: DateProfileGenerator }

  // derived state
  // TODO: make these all private
  private organizeRawLocales = memoize(organizeRawLocales)
  private buildDateEnv = memoize(buildDateEnv)
  private computeTitle = memoize(computeTitle)
  private buildTheme = memoize(buildTheme)
  private buildContext = memoize(buildContext)
  private buildEventUiSingleBase = memoize(buildEventUiSingleBase)
  private buildSelectionConfig = memoize(buildSelectionConfig)
  private buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqual)
  private buildEventUiBases = memoize(buildEventUiBases)
  private renderableEventStore: EventStore
  public eventUiBases: EventUiHash // needed for validation system
  public selectionConfig: EventUi // needed for validation system. doesn't need all the info EventUi provides. only validation-related
  private availableRawLocales: RawLocaleMap
  public context: ComponentContext
  public dateEnv: DateEnv
  public defaultAllDayEventDuration: Duration
  public defaultTimedEventDuration: Duration
  private resizeHandlers: ResizeHandler[] = []

  // interaction
  calendarInteractions: CalendarInteraction[]
  interactionsStore: { [componentUid: string]: Interaction[] } = {}

  state: CalendarState
  isRendering = false
  isRendered = false
  renderRunner: DelayedRunner
  actionRunner: TaskRunner<Action> // for reducer. bad name
  afterSizingTriggers: any = {}
  isViewUpdated: boolean = false
  isDatesUpdated: boolean = false
  isEventsUpdated: boolean = false
  el: HTMLElement
  componentRef = createRef<CalendarComponent>()
  view: ViewApi // public API

  get component() { return this.componentRef.current }


  constructor(el: HTMLElement, overrides?: OptionsInput) {
    this.el = el

    let optionsManager = this.optionsManager = new OptionsManager(overrides || {})
    this.pluginSystem = new PluginSystem()

    let renderRunner = this.renderRunner = new DelayedRunner(
      this.updateComponent.bind(this)
    )

    let actionRunner = this.actionRunner = new TaskRunner(
      this.runAction.bind(this),
      () => {
        this.updateDerivedState()
        renderRunner.request(optionsManager.computed.rerenderDelay)
      }
    )
    actionRunner.pause()

    // only do once. don't do in onOptionsChange. because can't remove plugins
    this.addPluginDefs(
      globalPlugins.concat(optionsManager.computed.plugins || [])
    )

    this.onOptionsChange()

    this.publiclyTrigger('_init') // for tests
    this.hydrate()
    actionRunner.resume()

    this.calendarInteractions = this.pluginSystem.hooks.calendarInteractions
      .map((calendarInteractionClass) => {
        return new calendarInteractionClass(this)
      })
  }


  addPluginDefs(pluginDefs: PluginDef[]) {
    for (let pluginDef of pluginDefs) {
      this.pluginSystem.add(pluginDef)
    }
  }


  // Public API for rendering
  // -----------------------------------------------------------------------------------------------------------------


  render() {
    if (!this.isRendering) {
      this.isRendering = true
      this.renderableEventStore = createEmptyEventStore()
      this.renderRunner.request()
      window.addEventListener('resize', this.handleWindowResize)
    } else {
      // hack for RERENDERING
      this.setOption('renderId', guid())
    }
  }


  destroy() {
    if (this.isRendering) {
      this.isRendering = false
      this.renderRunner.request()
      this.resizeRunner.clear()
      window.removeEventListener('resize', this.handleWindowResize)
    }
  }


  // Dispatcher
  // -----------------------------------------------------------------------------------------------------------------


  hydrate() {
    this.state = this.buildInitialState()

    let rawSources = this.opt('eventSources') || []
    let singleRawSource = this.opt('events')
    let sources = [] // parsed

    if (singleRawSource) {
      rawSources.unshift(singleRawSource)
    }

    for (let rawSource of rawSources) {
      let source = parseEventSource(rawSource, this)
      if (source) {
        sources.push(source)
      }
    }

    this.dispatch({ type: 'INIT' }) // pass in sources here?
    this.dispatch({ type: 'ADD_EVENT_SOURCES', sources })
    this.dispatch({
      type: 'SET_VIEW_TYPE',
      viewType: this.opt('defaultView') || this.pluginSystem.hooks.defaultView
    })
  }


  buildInitialState(): CalendarState {
    return {
      viewType: null,
      loadingLevel: 0,
      eventSourceLoadingLevel: 0,
      currentDate: this.getInitialDate(),
      dateProfile: null,
      eventSources: {},
      eventStore: createEmptyEventStore(),
      dateSelection: null,
      eventSelection: '',
      eventDrag: null,
      eventResize: null
    }
  }


  dispatch(action: Action) {
    this.actionRunner.request(action)

    // actions we know we want to render immediately. TODO: another param in dispatch instead?
    switch (action.type) {
      case 'SET_EVENT_DRAG':
      case 'SET_EVENT_RESIZE':
        this.renderRunner.drain()
    }
  }


  runAction(action: Action) {
    let oldState = this.state
    let newState = this.state = reduce(this.state, action, this)

    if (!oldState.loadingLevel && newState.loadingLevel) {
      this.publiclyTrigger('loading', [ true ])
    } else if (oldState.loadingLevel && !newState.loadingLevel) {
      this.publiclyTrigger('loading', [ false ])
    }

    let calendarComponent = this.component
    let viewComponent = calendarComponent && calendarComponent.view
    let viewApi = this.view // bad name

    if (oldState.eventStore !== newState.eventStore) {
      if (oldState.eventStore) {
        this.isEventsUpdated = true
      }
    }

    if (oldState.dateProfile !== newState.dateProfile) {
      if (oldState.dateProfile && viewComponent) { // why would view be null!?
        this.publiclyTrigger('datesDestroy', [
          {
            view: viewApi,
            el: viewComponent.base as HTMLElement
          }
        ])
      }
      this.isDatesUpdated = true
    }

    if (oldState.viewType !== newState.viewType) {
      if (oldState.viewType && viewComponent) { // why would view be null!?
        this.publiclyTrigger('viewSkeletonDestroy', [
          {
            view: viewApi,
            el: viewComponent.base as HTMLElement
          }
        ])
      }
      this.isViewUpdated = true
    }
  }


  // Rendering
  // -----------------------------------------------------------------------------------------------------------------


  batchRendering(func) {
    this.renderRunner.whilePaused(func)
  }


  updateComponent() {
    if (this.isRendering) {
      this.renderComponent()
      this.isRendered = true
    } else {
      if (this.isRendered) {
        this.destroyComponent()
        this.isRendered = false
      }
    }
  }


  renderComponent() {
    let { context, state } = this
    let { viewType } = state
    let viewSpec = this.viewSpecs[viewType]
    let viewApi = context.view

    // if event sources are still loading and progressive rendering hasn't been enabled,
    // keep rendering the last fully loaded set of events
    let renderableEventStore = this.renderableEventStore =
      (state.eventSourceLoadingLevel && !this.opt('progressiveEventRendering')) ?
        this.renderableEventStore :
        state.eventStore

    let eventUiSingleBase = this.buildEventUiSingleBase(viewSpec.options)
    let eventUiBySource = this.buildEventUiBySource(state.eventSources)
    let eventUiBases = this.eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource)

    render(
      <ComponentContextType.Provider value={context}>
        <CalendarComponent
          ref={this.componentRef}
          rootEl={this.el}
          { ...state }
          viewSpec={viewSpec}
          dateProfileGenerator={this.dateProfileGenerators[viewType]}
          dateProfile={state.dateProfile}
          eventStore={renderableEventStore}
          eventUiBases={eventUiBases}
          dateSelection={state.dateSelection}
          eventSelection={state.eventSelection}
          eventDrag={state.eventDrag}
          eventResize={state.eventResize}
          title={viewApi.title}
          />
      </ComponentContextType.Provider>,
      this.el
    )
    flushToDom()

    let calendarComponent = this.component
    let viewComponent = calendarComponent.view

    if (this.isViewUpdated) {
      this.isViewUpdated = false
      this.publiclyTrigger('viewSkeletonRender', [
        {
          view: viewApi,
          el: viewComponent.base as HTMLElement
        }
      ])
    }

    if (this.isDatesUpdated) {
      this.isDatesUpdated = false
      this.publiclyTrigger('datesRender', [
        {
          view: viewApi,
          el: viewComponent.base as HTMLElement
        }
      ])
    }

    if (this.isEventsUpdated) {
      this.isEventsUpdated = false
    }

    this.releaseAfterSizingTriggers()
  }


  destroyComponent() {
    render(null, this.el)

    for (let interaction of this.calendarInteractions) {
      interaction.destroy()
    }

    this.publiclyTrigger('_destroyed')
  }


  // Options
  // -----------------------------------------------------------------------------------------------------------------


  setOption(name: string, val) {
    this.mutateOptions({ [name]: val }, [], true)
  }


  getOption(name: string) { // getter, used externally
    return this.optionsManager.computed[name]
  }


  opt(name: string) { // getter, used internally
    return this.optionsManager.computed[name]
  }


  viewOpt(name: string) { // getter, used internally
    return this.viewSpecs[this.state.viewType].options[name]
  }

  /*
  handles option changes (like a diff)
  */
  mutateOptions(updates, removals: string[], isDynamic?: boolean, deepEqual?) {
    let changeHandlers = this.pluginSystem.hooks.optionChangeHandlers
    let normalUpdates = {}
    let specialUpdates = {}
    let oldDateEnv = this.dateEnv // do this before onOptionsChange
    let isTimeZoneDirty = false
    let anyDifficultOptions = Boolean(removals.length) // pretty much all options are "difficult" :(

    for (let name in updates) {
      if (changeHandlers[name]) {
        specialUpdates[name] = updates[name]
      } else {
        normalUpdates[name] = updates[name]
      }
    }

    for (let name in normalUpdates) {
      if (/^(defaultDate|defaultView)$/.test(name)) {
        // can't change date this way. use gotoDate instead
      } else {
        anyDifficultOptions = true // I guess all options are "difficult" ?

        if (name === 'timeZone') {
          isTimeZoneDirty = true
        }
      }
    }

    this.optionsManager.mutate(normalUpdates, removals, isDynamic)

    if (anyDifficultOptions) {
      this.onOptionsChange()
    }

    this.batchRendering(() => {

      if (anyDifficultOptions) {

        if (isTimeZoneDirty) {
          this.dispatch({
            type: 'CHANGE_TIMEZONE',
            oldDateEnv
          })
        }

        /* HACK
        has the same effect as calling this.updateComponent()
        but recomputes the state's dateProfile
        */
        this.dispatch({
          type: 'SET_VIEW_TYPE',
          viewType: this.state.viewType
        })

      }

      // special updates
      if (deepEqual) {
        for (let name in specialUpdates) {
          changeHandlers[name](specialUpdates[name], this, deepEqual)
        }
      }

    })
  }


  /*
  rebuilds things based off of a complete set of refined options
  TODO: move all this to updateDerivedState, but hard because reducer depends on some values
  */
  onOptionsChange() {
    let pluginHooks = this.pluginSystem.hooks
    let rawOptions = this.optionsManager.computed

    let availableLocaleData = this.organizeRawLocales(rawOptions.locales)
    let dateEnv = this.buildDateEnv(rawOptions, pluginHooks, availableLocaleData)

    this.availableRawLocales = availableLocaleData.map
    this.dateEnv = dateEnv
{'fraction_non_alphanumeric': 0.12605859105451353, 'fraction_numerical': 0.0001254626434978985, 'mean_word_length': 2.862733220256845, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 27, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4110950', 'n_tokens_mistral': 8907, 'n_tokens_neox': 8644, 'n_words': 2075}