text: import _debounce from 'lodash-es/debounce';
  
  import { dispatch as d3_dispatch } from 'd3-dispatch';
  import { json as d3_json } from 'd3-fetch';
  import { select as d3_select } from 'd3-selection';
  
  import { t } from '../core/localizer';
  
  import { fileFetcher as data } from './file_fetcher';
  import { localizer } from './localizer';
  import { prefs } from './preferences';
  import { coreHistory } from './history';
  import { coreValidator } from './validator';
  import { coreUploader } from './uploader';
  import { geoRawMercator } from '../geo/raw_mercator';
  import { modeSelect } from '../modes/select';
  import { presetManager } from '../presets';
  import { rendererBackground, rendererFeatures, rendererMap, rendererPhotos } from '../renderer';
  import { services } from '../services';
  import { uiInit } from '../ui/init';
  import { utilKeybinding, utilRebind, utilStringQs, utilUnicodeCharsTruncated } from '../util';
  
  
  export function coreContext() {
    const dispatch = d3_dispatch('enter', 'exit', 'change');
    let context = utilRebind({}, dispatch, 'on');
    let _deferred = new Set();
  
    context.version = '2.19.2-dev';
    context.privacyVersion = '20200407';
  
    // iD will alter the hash so cache the parameters intended to setup the session
    context.initialHashParams = window.location.hash ? utilStringQs(window.location.hash) : {};
  
    context.isFirstSession = !prefs('sawSplash') && !prefs('sawPrivacyVersion');
  
  
    /* Changeset */
    // An osmChangeset object. Not loaded until needed.
    context.changeset = null;
  
    let _defaultChangesetComment = context.initialHashParams.comment;
    let _defaultChangesetSource = context.initialHashParams.source;
    let _defaultChangesetHashtags = context.initialHashParams.hashtags;
    context.defaultChangesetComment = function(val) {
      if (!arguments.length) return _defaultChangesetComment;
      _defaultChangesetComment = val;
      return context;
    };
    context.defaultChangesetSource = function(val) {
      if (!arguments.length) return _defaultChangesetSource;
      _defaultChangesetSource = val;
      return context;
    };
    context.defaultChangesetHashtags = function(val) {
      if (!arguments.length) return _defaultChangesetHashtags;
      _defaultChangesetHashtags = val;
      return context;
    };
  
    /* Document title */
    /* (typically shown as the label for the browser window/tab) */
  
    // If true, iD will update the title based on what the user is doing
    let _setsDocumentTitle = true;
    context.setsDocumentTitle = function(val) {
      if (!arguments.length) return _setsDocumentTitle;
      _setsDocumentTitle = val;
      return context;
    };
    // The part of the title that is always the same
    let _documentTitleBase = document.title;
    context.documentTitleBase = function(val) {
      if (!arguments.length) return _documentTitleBase;
      _documentTitleBase = val;
      return context;
    };
  
  
    /* User interface and keybinding */
    let _ui;
    context.ui = () => _ui;
    context.lastPointerType = () => _ui.lastPointerType();
  
    let _keybinding = utilKeybinding('context');
    context.keybinding = () => _keybinding;
    d3_select(document).call(_keybinding);
  
  
    /* Straight accessors. Avoid using these if you can. */
    // Instantiate the connection here because it doesn't require passing in
    // `context` and it's needed for pre-init calls like `preauth`
    let _connection = services.osm;
    let _history;
    let _validator;
    let _uploader;
    context.connection = () => _connection;
    context.history = () => _history;
    context.validator = () => _validator;
    context.uploader = () => _uploader;
  
    /* Connection */
    context.preauth = (options) => {
      if (_connection) {
        _connection.switch(options);
      }
      return context;
    };
  
    /* connection options for source switcher (optional) */
    let _apiConnections;
    context.apiConnections = function(val) {
      if (!arguments.length) return _apiConnections;
      _apiConnections = val;
      return context;
    };
  
  
    // A string or array or locale codes to prefer over the browser's settings
    context.locale = function(locale) {
      if (!arguments.length) return localizer.localeCode();
      localizer.preferredLocaleCodes(locale);
      return context;
    };
  
  
    function afterLoad(cid, callback) {
      return (err, result) => {
        if (err) {
          // 400 Bad Request, 401 Unauthorized, 403 Forbidden..
          if (err.status === 400 || err.status === 401 || err.status === 403) {
            if (_connection) {
              _connection.logout();
            }
          }
          if (typeof callback === 'function') {
            callback(err);
          }
          return;
  
        } else if (_connection && _connection.getConnectionId() !== cid) {
          if (typeof callback === 'function') {
            callback({ message: 'Connection Switched', status: -1 });
          }
          return;
  
        } else {
          _history.merge(result.data, result.extent);
          if (typeof callback === 'function') {
            callback(err, result);
          }
          return;
        }
      };
    }
  
  
    context.loadTiles = (projection, callback) => {
      const handle = window.requestIdleCallback(() => {
        _deferred.delete(handle);
        if (_connection && context.editableDataEnabled()) {
          const cid = _connection.getConnectionId();
          _connection.loadTiles(projection, afterLoad(cid, callback));
        }
      });
      _deferred.add(handle);
    };
  
    context.loadTileAtLoc = (loc, callback) => {
      const handle = window.requestIdleCallback(() => {
        _deferred.delete(handle);
        if (_connection && context.editableDataEnabled()) {
          const cid = _connection.getConnectionId();
          _connection.loadTileAtLoc(loc, afterLoad(cid, callback));
        }
      });
      _deferred.add(handle);
    };
  
    context.loadEntity = (entityID, callback) => {
      if (_connection) {
        const cid = _connection.getConnectionId();
        _connection.loadEntity(entityID, afterLoad(cid, callback));
      }
    };
  
    context.zoomToEntity = (entityID, zoomTo) => {
      if (zoomTo !== false) {
        context.loadEntity(entityID, (err, result) => {
          if (err) return;
          const entity = result.data.find(e => e.id === entityID);
          if (entity) {
            _map.zoomTo(entity);
          }
        });
      }
  
      _map.on('drawn.zoomToEntity', () => {
        if (!context.hasEntity(entityID)) return;
        _map.on('drawn.zoomToEntity', null);
        context.on('enter.zoomToEntity', null);
        context.enter(modeSelect(context, [entityID]));
      });
  
      context.on('enter.zoomToEntity', () => {
        if (_mode.id !== 'browse') {
          _map.on('drawn.zoomToEntity', null);
          context.on('enter.zoomToEntity', null);
        }
      });
    };
  
    let _minEditableZoom = 16;
    context.minEditableZoom = function(val) {
      if (!arguments.length) return _minEditableZoom;
      _minEditableZoom = val;
      if (_connection) {
        _connection.tileZoom(val);
      }
      return context;
    };
  
    // String length limits in Unicode characters, not JavaScript UTF-16 code units
    context.maxCharsForTagKey = () => 255;
    context.maxCharsForTagValue = () => 255;
    context.maxCharsForRelationRole = () => 255;
  
    function cleanOsmString(val, maxChars) {
      // be lenient with input
      if (val === undefined || val === null) {
        val = '';
      } else {
        val = val.toString();
      }
  
      // remove whitespace
      val = val.trim();
  
      // use the canonical form of the string
      if (val.normalize) val = val.normalize('NFC');
  
      // trim to the number of allowed characters
      return utilUnicodeCharsTruncated(val, maxChars);
    }
    context.cleanTagKey = (val) => cleanOsmString(val, context.maxCharsForTagKey());
    context.cleanTagValue = (val) => cleanOsmString(val, context.maxCharsForTagValue());
    context.cleanRelationRole = (val) => cleanOsmString(val, context.maxCharsForRelationRole());
  
  
    /* History */
    let _inIntro = false;
    context.inIntro = function(val) {
      if (!arguments.length) return _inIntro;
      _inIntro = val;
      return context;
    };
  
    // Immediately save the user's history to localstorage, if possible
    // This is called someteimes, but also on the `window.onbeforeunload` handler
    context.save = () => {
      // no history save, no message onbeforeunload
      if (_inIntro || context.container().select('.modal').size()) return;
  
      let canSave;
      if (_mode && _mode.id === 'save') {
        canSave = false;
  
        // Attempt to prevent user from creating duplicate changes - see #5200
        if (services.osm && services.osm.isChangesetInflight()) {
          _history.clearSaved();
          return;
        }
  
      } else {
        canSave = context.selectedIDs().every(id => {
          const entity = context.hasEntity(id);
          return entity && !entity.isDegenerate();
        });
      }
  
      if (canSave) {
        _history.save();
      }
      if (_history.hasChanges()) {
        return t('save.unsaved_changes');
      }
    };
  
    // Debounce save, since it's a synchronous localStorage write,
    // and history changes can happen frequently (e.g. when dragging).
    context.debouncedSave = _debounce(context.save, 350);
  
    function withDebouncedSave(fn) {
      return function() {
        const result = fn.apply(_history, arguments);
        context.debouncedSave();
        return result;
      };
    }
  
  
    /* Graph */
    context.hasEntity = (id) => _history.graph().hasEntity(id);
    context.entity = (id) => _history.graph().entity(id);
  
  
    /* Modes */
    let _mode;
    context.mode = () => _mode;
    context.enter = (newMode) => {
      if (_mode) {
        _mode.exit();
        dispatch.call('exit', this, _mode);
      }
  
      _mode = newMode;
      _mode.enter();
      dispatch.call('enter', this, _mode);
    };
  
    context.selectedIDs = () => (_mode && _mode.selectedIDs && _mode.selectedIDs()) || [];
    context.activeID = () => _mode && _mode.activeID && _mode.activeID();
  
    let _selectedNoteID;
    context.selectedNoteID = function(noteID) {
      if (!arguments.length) return _selectedNoteID;
      _selectedNoteID = noteID;
      return context;
    };
  
    // NOTE: Don't change the name of this until UI v3 is merged
    let _selectedErrorID;
    context.selectedErrorID = function(errorID) {
      if (!arguments.length) return _selectedErrorID;
      _selectedErrorID = errorID;
      return context;
    };
  
  
    /* Behaviors */
    context.install = (behavior) => context.surface().call(behavior);
    context.uninstall = (behavior) => context.surface().call(behavior.off);
  
  
    /* Copy/Paste */
    let _copyGraph;
    context.copyGraph = () => _copyGraph;
  
    let _copyIDs = [];
    context.copyIDs = function(val) {
      if (!arguments.length) return _copyIDs;
      _copyIDs = val;
      _copyGraph = _history.graph();
      return context;
    };
  
    let _copyLonLat;
    context.copyLonLat = function(val) {
      if (!arguments.length) return _copyLonLat;
      _copyLonLat = val;
      return context;
    };
  
  
    /* Background */
    let _background;
    context.background = () => _background;
  
  
    /* Features */
    let _features;
    context.features = () => _features;
    context.hasHiddenConnections = (id) => {
      const graph = _history.graph();
      const entity = graph.entity(id);
      return _features.hasHiddenConnections(entity, graph);
    };
  
  
    /* Photos */
    let _photos;
    context.photos = () => _photos;
  
  
    /* Map */
    let _map;
    context.map = () => _map;
    context.layers = () => _map.layers();
    context.surface = () => _map.surface;
    context.editableDataEnabled = () => _map.editableDataEnabled();
    context.surfaceRect = () => _map.surface.node().getBoundingClientRect();
    context.editable = () => {
      // don't allow editing during save
      const mode = context.mode();
      if (!mode || mode.id === 'save') return false;
      return _map.editableDataEnabled();
    };
  
  
    /* Debug */
    let _debugFlags = {
      tile: false,        // tile boundaries
      collision: false,   // label collision bounding boxes
      imagery: false,     // imagery bounding polygons
      target: false,      // touch targets
      downloaded: false   // downloaded data from osm
    };
    context.debugFlags = () => _debugFlags;
    context.getDebug = (flag) => flag && _debugFlags[flag];
    context.setDebug = function(flag, val) {
      if (arguments.length === 1) val = true;
      _debugFlags[flag] = val;
      dispatch.call('change');
      return context;
    };
  
  
    /* Container */
    let _container = d3_select(null);
    context.container = function(val) {
      if (!arguments.length) return _container;
      _container = val;
      _container.classed('ideditor', true);
      return context;
    };
    context.containerNode = function(val) {
      if (!arguments.length) return context.container().node();
      context.container(d3_select(val));
      return context;
    };
  
    let _embed;
    context.embed = function(val) {
      if (!arguments.length) return _embed;
      _embed = val;
      return context;
    };
  
  
    /* Assets */
    let _assetPath = '';
    context.assetPath = function(val) {
      if (!arguments.length) return _assetPath;
      _assetPath = val;
      data.assetPath(val);
      return context;
    };
  
    let _assetMap = {};
    context.assetMap = function(val) {
      if (!arguments.length) return _assetMap;
      _assetMap = val;
      data.assetMap(val);
      return context;
    };
  
    context.asset = (val) => {
      if (/^http(s)?:\/\//i.test(val)) return val;
      const filename = _assetPath + val;
      return _assetMap[filename] || filename;
    };
  
    context.imagePath = (val) => context.asset(`img/${val}`);
  
  
    /* reset (aka flush) */
    context.reset = context.flush = () => {
      context.debouncedSave.cancel();
  
      Array.from(_deferred).forEach(handle => {
        window.cancelIdleCallback(handle);
        _deferred.delete(handle);
      });
  
      Object.values(services).forEach(service => {
        if (service && typeof service.reset === 'function') {
          service.reset(context);
        }
      });
  
      context.changeset = null;
  
      _validator.reset();
      _features.reset();
      _history.reset();
      _uploader.reset();
  
      // don't leave stale state in the inspector
      context.container().select('.inspector-wrap *').remove();
  
      return context;
    };
  
  
    /* Projections */
    context.projection = geoRawMercator();
    context.curtainProjection = geoRawMercator();
  
  
    /* Init */
    context.init = () => {
  
      instantiateInternal();
  
      initializeDependents();
  
      return context;
  
      // Load variables and properties. No property of `context` should be accessed
      // until this is complete since load statuses are indeterminate. The order
      // of instantiation shouldn't matter.
      function instantiateInternal() {
  
        _history = coreHistory(context);
        context.graph = _history.graph;
        context.pauseChangeDispatch = _history.pauseChangeDispatch;
        context.resumeChangeDispatch = _history.resumeChangeDispatch;
        context.perform = withDebouncedSave(_history.perform);
        context.replace = withDebouncedSave(_history.replace);
        context.pop = withDebouncedSave(_history.pop);
        context.overwrite = withDebouncedSave(_history.overwrite);
        context.undo = withDebouncedSave(_history.undo);
        context.redo = withDebouncedSave(_history.redo);
  
        _validator = coreValidator(context);
        _uploader = coreUploader(context);
  
        _background = rendererBackground(context);
        _features = rendererFeatures(context);
        _map = rendererMap(context);
        _photos = rendererPhotos(context);
  
        _ui = uiInit(context);
      }
  
      // Set up objects that might need to access properties of `context`. The order
      // might matter if dependents make calls to each other. Be wary of async calls.
      function initializeDependents() {
  
        if (context.initialHashParams.presets) {
          presetManager.addablePresetIDs(new Set(context.initialHashParams.presets.split(',')));
        }
  
        if (context.initialHashParams.locale) {
          localizer.preferredLocaleCodes(context.initialHashParams.locale);
        }
  
        // kick off some async work
        localizer.ensureLoaded();
        _background.ensureLoaded();
        presetManager.ensureLoaded();
  
        Object.values(services).forEach(service => {
          if (service && typeof service.init === 'function') {
            service.init();
          }
        });
  
        _map.init();
        _validator.init();
        _features.init();
  
        if (services.maprules && context.initialHashParams.maprules) {
          d3_json(context.initialHashParams.maprules)
            .then(mapcss => {
              services.maprules.init();
              mapcss.forEach(mapcssSelector => services.maprules.addRule(mapcssSelector));
            })
            .catch(() => { /* ignore */ });
        }
  
        // if the container isn't available, e.g. when testing, don't load the UI
        if (!context.container().empty()) {
          _ui.ensureLoaded().then(function() {
            _photos.init();
          });
        }
      }
    };
  
  
    return context;
  }
  