# Source: src/js/models/maps/assets/CesiumImagery.js

    'use strict';

    define(
      [
        'jquery',
        'underscore',
        'backbone',
        'cesium',
        'models/maps/assets/MapAsset',
      ],
      function (
        $,
        _,
        Backbone,
        Cesium,
        MapAsset
      ) {
        /**
         * @classdesc A CesiumImagery Model contains the information required for Cesium to
         * request and draw high-resolution image tiles using several standards (Cesium
         * "imagery providers"), including Cesium Ion and Bing Maps. Imagery layers have
         * brightness, contrast, gamma, hue, and saturation properties that can be dynamically
         * changed.
         * @classcategory Models/Maps/Assets
         * @class CesiumImagery
         * @name CesiumImagery
         * @extends MapAsset
         * @since 2.18.0
         * @constructor
        */
        var CesiumImagery = MapAsset.extend(
          /** @lends CesiumImagery.prototype */ {

            /**
             * The name of this type of model
             * @type {string}
            */
            type: 'CesiumImagery',

            /**
             * Options that are supported for creating imagery tiles. Any properties provided
             * here are passed to the Cesium constructor function, so other properties that
             * are documented in Cesium are also supported. See
             * {@link https://cesium.com/learn/cesiumjs/ref-doc/BingMapsImageryProvider.html#.ConstructorOptions}
             * and
             * {@link https://cesium.com/learn/cesiumjs/ref-doc/IonImageryProvider.html#.ConstructorOptions}.
             * @typedef {Object} CesiumImagery#cesiumOptions
             * @property {string|number} ionAssetId - If this imagery is hosted by Cesium
             * Ion, then Ion asset ID.
             * @property {string|number} key - A key or token required to access the tiles.
             * For example, if this is a BingMapsImageryProvider, then the Bing maps key. If
             * one is required and not set, the model will look in the {@link AppModel} for a
             * key, for example, {@link AppModel#bingMapsKey}
             * @property {'GeographicTilingScheme'|'WebMercatorTilingScheme'} tilingScheme -
             * The tiling scheme to use when constructing an imagery provider. If not set,
             * Cesium uses WebMercatorTilingScheme by default.
             * @property {Number[]} rectangle - The rectangle covered by the layer. The list
             * of west, south, east, north bounding degree coordinates, respectively. This
             * will be passed to Cesium.Rectangle.fromDegrees to define the bounding box of
             * the imagery layer. If left undefined, the layer will cover the entire globe.
             */

            /**
             * Default attributes for CesiumImagery models
             * @name CesiumImagery#defaults
             * @extends MapAsset#defaults
             * @type {Object}
             * @property {'BingMapsImageryProvider'|'IonImageryProvider'|'TileMapServiceImageryProvider'|'WebMapTileServiceImageryProvider'} type
             * A string indicating a Cesium Imagery Provider type. See
             * {@link https://cesium.com/learn/cesiumjs-learn/cesiumjs-imagery/#more-imagery-providers}
             * @property {Cesium.ImageryLayer} cesiumModel A model created and used by Cesium
             * that organizes the data to display in the Cesium Widget. See
             * {@link https://cesium.com/learn/cesiumjs/ref-doc/ImageryLayer.html?classFilter=ImageryLayer}
             * and
             * {@link https://cesium.com/learn/cesiumjs/ref-doc/?classFilter=ImageryProvider}
             * @property {CesiumImagery#cesiumOptions} cesiumOptions options that are passed
             * to the function that creates the Cesium model. The properties of options are
             * specific to each type of asset.
            */
            defaults: function () {
              return _.extend(
                this.constructor.__super__.defaults(),
                {
                  type: '',
                  cesiumModel: null,
                  cesiumOptions: {},
                  // brightness: 1, contrast: 1, gamma: 1, hue: 0, saturation: 1,
                }
              );
            },

            /**
             * Executed when a new CesiumImagery model is created.
             * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
             * attributes, which will be set on the model.
             */
            initialize: function (assetConfig) {
              try {
                MapAsset.prototype.initialize.call(this, assetConfig);

                if (assetConfig.type == 'NaturalEarthII') {
                  this.initNaturalEarthII(assetConfig);
                }
                else if (assetConfig.type == 'USGSImageryTopo') {
                  this.initUSGSImageryTopo(assetConfig);
                }

                this.createCesiumModel();

                this.getThumbnail();
              }
              catch (error) {
                console.log(
                  'There was an error initializing a CesiumImagery model' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Initializes a CesiumImagery model for the Natural Earth II asset.
             * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
             * attributes, which will be set on the model.
             */
            initNaturalEarthII: function (assetConfig) {
              try {
                if (
                  !assetConfig.cesiumOptions || typeof assetConfig.cesiumOptions !== 'object'
                ) {
                  assetConfig.cesiumOptions = {};
                }

                assetConfig.cesiumOptions.url = Cesium.buildModuleUrl(
                  'Assets/Textures/NaturalEarthII'
                );
                this.set('type', 'TileMapServiceImageryProvider')
                this.set('cesiumOptions', assetConfig.cesiumOptions);
              }
              catch (error) {
                console.log(
                  'There was an error initializing NaturalEarthII in a CesiumImagery' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Initializes a CesiumImagery model for the USGS Imagery Topo asset.
             * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
             * attributes, which will be set on the model.
             */
            initUSGSImageryTopo: function (assetConfig) {
              try {
                if (
                  !assetConfig.cesiumOptions || typeof assetConfig.cesiumOptions !== 'object'
                ) {
                  assetConfig.cesiumOptions = {};
                }
                this.set('type', 'WebMapServiceImageryProvider')
                assetConfig.cesiumOptions.url = 'https://basemap.nationalmap.gov:443/arcgis/services/USGSImageryTopo/MapServer/WmsServer'
                assetConfig.cesiumOptions.layers = '0'
                assetConfig.cesiumOptions.parameters = {
                  transparent: true,
                  format: 'image/png',
                }
                this.set('cesiumOptions', assetConfig.cesiumOptions);
                if (!assetConfig.moreInfoLink) {
                  this.set('moreInfoLink', 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer')
                }
                if (!assetConfig.attribution) {
                  this.set('attribution', 'USGS The National Map: Orthoimagery and US Topo. Data refreshed January, 2022.')
                }
                if (!assetConfig.description) {
                  this.set('description', 'USGS Imagery Topo is a tile cache base map of orthoimagery in The National Map and US Topo vectors visible to the 1:9,028 scale.')
                }
                if (!assetConfig.label) {
                  this.set('label', 'USGS Imagery Topo')
                }
              }
              catch (error) {
                console.log(
                  'There was an error initializing USGSImageryTopo in a CesiumImagery' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Creates a Cesium ImageryLayer that contains information about how the imagery
             * should render in Cesium. See
             * {@link https://cesium.com/learn/cesiumjs/ref-doc/ImageryLayer.html?classFilter=ImageryLay}
             * @param {Boolean} recreate - Set recreate to true to force the function create
             * the Cesium Model again. Otherwise, if a cesium model already exists, that is
             * returned instead.
             */
            createCesiumModel: function (recreate = false) {

              var model = this;
              var cesiumOptions = this.get('cesiumOptions');
              var type = this.get('type')
              var providerFunction = Cesium[type]

              // If the cesium model already exists, don't create it again unless specified
              if (!recreate && this.get('cesiumModel')) {
                console.log('returning existing cesium model');
                return this.get('cesiumModel')
              }

              model.resetStatus();

              var initialAppearance = {
                alpha: this.get('opacity'),
                show: this.get('visible')
                // TODO: brightness, contrast, gamma, etc.
              }

              if (type === 'BingMapsImageryProvider') {
                cesiumOptions.key = cesiumOptions.key || MetacatUI.AppConfig.bingMapsKey
              } else if (type === 'IonImageryProvider') {
                cesiumOptions.assetId = Number(cesiumOptions.ionAssetId)
                delete cesiumOptions.ionAssetId
                cesiumOptions.accessToken =
                  cesiumOptions.cesiumToken || MetacatUI.appModel.get('cesiumToken');
              }
              if (cesiumOptions && cesiumOptions.tilingScheme) {
                const ts = cesiumOptions.tilingScheme
                const availableTS = ['GeographicTilingScheme', 'WebMercatorTilingScheme']
                if (availableTS.indexOf(ts) > -1) {
                  cesiumOptions.tilingScheme = new Cesium[ts]()
                } else {
                  console.log(`${ts} is not a valid tiling scheme. Using WebMercatorTilingScheme`)
                  cesiumOptions.tilingScheme = new Cesium.WebMercatorTilingScheme()
                }
              }

              if (cesiumOptions.rectangle) {
                cesiumOptions.rectangle = Cesium.Rectangle.fromDegrees(
                  ...cesiumOptions.rectangle
                )
              }

              if (providerFunction && typeof providerFunction === 'function') {
                let provider = new providerFunction(cesiumOptions)
                provider.readyPromise
                  .then(function () {
                    // Imagery must be converted from a Cesium Imagery Provider to a Cesium
                    // Imagery Layer. See
                    // https://cesium.com/learn/cesiumjs-learn/cesiumjs-imagery/#imagery-providers-vs-layers
                    model.set('cesiumModel', new Cesium.ImageryLayer(provider, initialAppearance))
                    model.set('status', 'ready')
                    model.setListeners()
                  })
                  .otherwise(function (error) {
                    // See https://cesium.com/learn/cesiumjs/ref-doc/RequestErrorEvent.html
                    let details = error;
                    // Write a helpful error message
                    switch (error.statusCode) {
                      case 404:
                        details = 'The resource was not found (error code 404).'
                        break;
                      case 500:
                        details = 'There was a server error (error code 500).'
                        break;
                    }
                    model.set('status', 'error');
                    model.set('statusDetails', details)
                  })
              } else {
                model.set('status', 'error')
                model.set('statusDetails', type + ' is not a supported imagery type.')
              }

            },

            /**
             * Set listeners that update the cesium model when the backbone model is updated.
             */
            setListeners: function () {
              try {

                var cesiumModel = this.get('cesiumModel')

                // Make sure the listeners are only set once!
                this.stopListening(this);

                this.listenTo(this, 'change:opacity', function (model, opacity) {
                  cesiumModel.alpha = opacity
                  // Let the map and/or other parent views know that a change has been made
                  // that requires the map to be re-rendered
                  model.trigger('appearanceChanged')

                })
                this.listenTo(this, 'change:visible', function (model, visible) {
                  cesiumModel.show = visible
                  // Let the map and/or other parent views know that a change has been made
                  // that requires the map to be re-rendered
                  model.trigger('appearanceChanged')
                })
              }
              catch (error) {
                console.log(
                  'There was an error setting listeners in a cesium Imagery model' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Gets a Cesium Bounding Sphere that can be used to navigate to view the full
             * extent of the imagery. See
             * {@link https://cesium.com/learn/cesiumjs/ref-doc/BoundingSphere.html}
             * @returns {Promise} Returns a promise that resolves to a Cesium Bounding Sphere
             * when ready
             */
            getBoundingSphere: function () {
              return this.whenReady()
                .then(function (model) {
                  return model.get('cesiumModel').getViewableRectangle()
                })
                .then(function (rectangle) {
                  return Cesium.BoundingSphere.fromRectangle3D(rectangle)
                })
            },

            /**
             * Requests a tile from the imagery provider that is at the center of the layer's
             * bounding box and at the minimum level. Once the image is fetched, sets its URL
             * on the thumbnail property of this model. This function is first called when the
             * layer initialized, but waits for the cesiumModel to be ready.
             */
            getThumbnail: function () {
              try {
                if (this.get('status') !== 'ready') {
                  this.listenToOnce(this, 'change:status', this.getThumbnail)
                  return
                }

                const model = this
                const cesImageryLayer = this.get('cesiumModel');
                const provider = cesImageryLayer.imageryProvider
                const rect = cesImageryLayer.rectangle
                var x = (rect.east + rect.west) / 2
                var y = (rect.north + rect.south) / 2
                var level = provider.minimumLevel

                provider.requestImage(x, y, level).then(function (response) {

                  let data = response.blob
                  let objectURL = null

                  if (!data && response instanceof ImageBitmap) {
                    objectURL = model.getDataUriFromBitmap(response)
                  } else {
                    objectURL = URL.createObjectURL(data);
                  }

                  model.set('thumbnail', objectURL)
                }).otherwise(function (e) {
                  console.log('Error requesting an image tile to use as a thumbnail for an ' +
                    'Imagery Layer. Error message: ' + e);
                })
              }
              catch (error) {
                console.log(
                  'There was an error getting a thumbnail for a CesiumImagery layer' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Gets a data URI from a bitmap image.
             * @param {ImageBitmap} bitmap The bitmap image to convert to a data URI
             * @returns {String} Returns a string containing the requested data URI.
            */
            getDataUriFromBitmap: function (imageBitmap) {
              try {
                const canvas = document.createElement('canvas');
                canvas.width = imageBitmap.width;
                canvas.height = imageBitmap.height;
                const ctx = canvas.getContext('2d')
                // y-flip the image - Natural Earth II bitmaps appear upside down otherwise
                // TODO: Test with other imagery layers
                ctx.translate(0, imageBitmap.height);
                ctx.scale(1, -1);
                ctx.drawImage(imageBitmap, 0, 0);
                return canvas.toDataURL();
              } catch (error) {
                console.log(
                  'There was an error converting an ImageBitmap to a data URL' +
                  '. Error details: ' + error
                );
              }
            },

            // /**
            //  * Parses the given input into a JSON object to be set on the model.
            //  *
            //  * @param {TODO} input - The raw response object
            //  * @return {TODO} - The JSON object of all the Imagery attributes
            //    */
            // parse: function (input) {

            //   try {

            //     var modelJSON = {};

            //     return modelJSON

            //   }
            //   catch (error) {console.log('There was an error parsing a Imagery model' + '.
            //     Error details: ' + error
            //     );
            //   }

            // },

            // /**
            //  * Overrides the default Backbone.Model.validate.function() to check if this if
            //  * the values set on this model are valid.
            //  *
            //  * @param {Object} [attrs] - A literal object of model attributes to validate.
            //  * @param {Object} [options] - A literal object of options for this validation
            //  * process
            //  *
            //  * @return {Object} - Returns a literal object with the invalid attributes and
            //  * their corresponding error message, if there are any. If there are no errors,
            //  * returns nothing.
            //    */
            // validate: function (attrs, options) {try {

            //   }
            //   catch (error) {console.log('There was an error validating a CesiumImagery
            //     model' + '. Error details: ' + error
            //     );
            //   }
            // },

            // /**
            //  * Creates a string using the values set on this model's attributes.
            //  * @return {string} The Imagery string
            //    */
            // serialize: function () {try {var serializedImagery = "";

            //     return serializedImagery;
            //   }
            //   catch (error) {console.log('There was an error serializing a CesiumImagery
            //     model' + '. Error details: ' + error
            //     );
            //   }
            // },

          });

        return CesiumImagery;

      }
    );