(function () {
  'use strict';

  var push = Array.prototype.push;
  var noop = angular.noop;

  /**
   * @ngdoc overview
   * @name hz.dashboard.launch-instance
   *
   * @description
   * Manage workflow of creating server.
   */

  var module = angular.module('hz.dashboard.launch-instance');

  /**
   * @ngdoc service
   * @name launchInstanceModel
   *
   * @description
   * This is the M part in MVC design pattern for launch instance
   * wizard workflow. It is responsible for providing data to the
   * view of each step in launch instance workflow and collecting
   * user's input from view for  creation of new instance.  It is
   * also the center point of communication between launch instance
   * UI and services API.
   */

  module.factory('launchInstanceModel', ['$q', '$log',
    'horizon.openstack-service-api.cinder',
    'horizon.openstack-service-api.glance',
    'horizon.openstack-service-api.keystone',
    'horizon.openstack-service-api.neutron',
    'horizon.openstack-service-api.nova',
    'horizon.openstack-service-api.novaExtensions',
    'horizon.openstack-service-api.security-group',
    'horizon.openstack-service-api.serviceCatalog',

    function ($q,
              $log,
              cinderAPI,
              glanceAPI,
              keystoneAPI,
              neutronAPI,
              novaAPI,
              novaExtensions,
              securityGroup,
              serviceCatalog) {

      var initPromise;

      // Constants (const in ES6)
      var NON_BOOTABLE_IMAGE_TYPES = ['aki', 'ari'];
      var SOURCE_TYPE_IMAGE = 'image';
      var SOURCE_TYPE_SNAPSHOT = 'snapshot';
      var SOURCE_TYPE_VOLUME = 'volume';
      var SOURCE_TYPE_VOLUME_SNAPSHOT = 'volume_snapshot';

      /**
       * @ngdoc model api object
       */
      var model = {

        initializing: false,
        initialized: false,

        /**
         * @name newInstanceSpec
         *
         * @description
         * A dictionary like object containing specification collected from user's
         * input.  Its required properties include:
         *
         * @property {String} name: The new server name.
         * @property {String} source_type: The type of source
         *   Valid options: (image | snapshot | volume | volume_snapshot)
         * @property {String} source_id: The ID of the image / volume to use.
         * @property {String} flavor_id: The ID of the flavor to use.
         *
         * Other parameters are accepted as per the underlying novaclient:
         *  - https://github.com/openstack/python-novaclient/blob/master/novaclient/v2/servers.py#L417
         * But may be required additional values as per nova:
         *  - https://github.com/openstack/horizon/blob/master/openstack_dashboard/api/rest/nova.py#L127
         *
         * The JS code only needs to set the values below as they are made.
         * The createInstance function will map them appropriately.
         */

        // see initializeNewInstanceSpec
        newInstanceSpec: {},

        /**
         * cloud service properties, they should be READ-ONLY to all UI controllers
         */

        availabilityZones: [],
        flavors: [],
        allowedBootSources: [],
        images: [],
        allowCreateVolumeFromImage: false,
        arePortProfilesSupported: false,
        imageSnapshots: [],
        keypairs: [],
        metadataDefs: {
          flavor: null,
          image: null,
          volume: null
        },
        networks: [],
        neutronEnabled: false,
        novaLimits: {},
        profiles: [],
        securityGroups: [],
        volumeBootable: false,
        volumes: [],
        volumeSnapshots: [],

        /**
         * api methods for UI controllers
         */

        initialize: initialize,
        createInstance: createInstance
      };

      // Local function.
      function initializeNewInstanceSpec() {

        model.newInstanceSpec = {
          availability_zone: null,
          admin_pass: null,
          config_drive: false,
          user_data: '',                  // REQUIRED Server Key.  Null allowed.
          disk_config: 'AUTO',
          flavor: null,                   // REQUIRED
          instance_count: 1,
          key_pair: [],                   // REQUIRED Server Key
          name: null,                     // REQUIRED
          networks: [],
          profile: {},
          security_groups: [],            // REQUIRED Server Key. May be empty.
          source_type: null,              // REQUIRED for JS logic (image | snapshot | volume | volume_snapshot)
          source: [],
          vol_create: false,              // REQUIRED for JS logic
          vol_device_name: 'vda',         // May be null
          vol_delete_on_terminate: false,
          vol_size: 1
        };
      }

      /**
       * @ngdoc method
       * @name launchInstanceModel.initialize
       * @returns {promise}
       *
       * @description
       * Send request to get all data to initialize the model.
       */

      function initialize(deep) {
        var deferred, promise;

        // Each time opening launch instance wizard, we need to do this, or
        // we can call the whole methods `reset` instead of `initialize`.
        initializeNewInstanceSpec();

        if (model.initializing) {
          promise = initPromise;

        } else if (model.initialized && !deep) {
          deferred = $q.defer();
          promise = deferred.promise;
          deferred.resolve();

        } else {
          model.initializing = true;

          model.allowedBootSources.length = 0;

          promise = $q.all([
            getImages(),
            novaAPI.getAvailabilityZones().then(onGetAvailabilityZones, noop),
            novaAPI.getFlavors(true, true).then(onGetFlavors, noop),
            novaAPI.getKeypairs().then(onGetKeypairs, noop),
            novaAPI.getLimits().then(onGetNovaLimits, noop),
            securityGroup.query().then(onGetSecurityGroups, noop),
            serviceCatalog.ifTypeEnabled('network').then(getNetworks, noop),
            serviceCatalog.ifTypeEnabled('volume').then(getVolumes, noop)
          ]);

          promise.then(
            function() {
              model.initializing = false;
              model.initialized = true;
              // This provides supplemental data non-critical to launching
              // an instance.  Therefore we load it only if the critical data
              // all loads successfully.
              getMetadataDefinitions();
            },
            function () {
              model.initializing = false;
              model.initialized = false;
            }
          );
        }

        return promise;
      }

      /**
       * @ngdoc method
       * @name launchInstanceModel.createInstance
       * @returns {promise}
       *
       * @description
       * Send request for creating server.
       */

      function createInstance() {
        var finalSpec = angular.copy(model.newInstanceSpec);

        cleanNullProperties();

        setFinalSpecBootsource(finalSpec);
        setFinalSpecFlavor(finalSpec);
        setFinalSpecNetworks(finalSpec);
        setFinalSpecKeyPairs(finalSpec);
        setFinalSpecSecurityGroups(finalSpec);

        return novaAPI.createServer(finalSpec);
      }

      function cleanNullProperties(finalSpec) {
        // Initially clean fields that don't have any value.
        for (var key in finalSpec) {
          if (finalSpec.hasOwnProperty(key)  && finalSpec[key] === null) {
            delete finalSpec[key];
          }
        }
      }

      //
      // Local
      //

      function onGetAvailabilityZones(data) {
        model.availabilityZones.length = 0;
        push.apply(model.availabilityZones, data.data.items
          .filter(function (zone) {
            return zone.zoneState && zone.zoneState.available;
          })
          .map(function (zone) {
            return zone.zoneName;
          })
        );

        if (model.availabilityZones.length > 0) {
          model.newInstanceSpec.availability_zone = model.availabilityZones[0];
        }
      }

      // Flavors

      function onGetFlavors(data) {
        model.flavors.length = 0;
        push.apply(model.flavors, data.data.items);
      }

      function setFinalSpecFlavor(finalSpec) {
        if ( finalSpec.flavor ) {
          finalSpec.flavor_id = finalSpec.flavor.id;
        } else {
          delete finalSpec.flavor_id;
        }

        delete finalSpec.flavor;
      }

      // Keypairs

      function onGetKeypairs(data) {
        angular.extend(
          model.keypairs,
          data.data.items.map(function (e) {
            e.keypair.id = e.keypair.name;
            return e.keypair;
          }));
      }

      function setFinalSpecKeyPairs(finalSpec) {
        // Nova only wants the key name. It is a required field, even if None.
        if (!finalSpec.key_name && finalSpec.key_pair.length === 1) {
          finalSpec.key_name = finalSpec.key_pair[0].name;
        } else if (!finalSpec.key_name) {
          finalSpec.key_name = null;
        }

        delete finalSpec.key_pair;
      }

      // Security Groups

      function onGetSecurityGroups(data) {
        model.securityGroups.length = 0;
        push.apply(model.securityGroups, data.data.items);
        // set initial default
        if (model.newInstanceSpec.security_groups.length === 0 &&
            model.securityGroups.length > 0) {
          model.securityGroups.forEach(function (securityGroup) {
            if (securityGroup.name === 'default') {
              model.newInstanceSpec.security_groups.push(securityGroup);
            }
          });
        }
      }

      function setFinalSpecSecurityGroups(finalSpec) {
        // pull out the ids from the security groups objects
        var securityGroupIds = [];
        finalSpec.security_groups.forEach(function(securityGroup) {
          if (model.neutronEnabled) {
            securityGroupIds.push(securityGroup.id);
          } else {
            securityGroupIds.push(securityGroup.name);
          }
        });
        finalSpec.security_groups = securityGroupIds;
      }

      // Networks

      function getNetworks() {
        return neutronAPI.getNetworks().then(onGetNetworks, noop);
      }

      function onGetNetworks(data) {
        model.neutronEnabled = true;
        model.networks.length = 0;
        push.apply(model.networks, data.data.items);
      }

      function setFinalSpecNetworks(finalSpec) {
        finalSpec.nics = [];
        finalSpec.networks.forEach(function (network) {
          finalSpec.nics.push(
            {
              "net-id": network.id,
              "v4-fixed-ip": ""
            });
        });
        delete finalSpec.networks;
      }

      // Boot Source

      function getImages() {
        return glanceAPI.getImages({status:'active'}).then(onGetImages);
      }

      function isBootableImageType(image) {
        // This is a blacklist of images that can not be booted.
        // If the image container type is in the blacklist
        // The evaluation will result in a 0 or greater index.
        return NON_BOOTABLE_IMAGE_TYPES.indexOf(image.container_format) < 0;
      }

      function onGetImages(data) {
        model.images.length = 0;
        push.apply(model.images, data.data.items.filter(function (image) {
          return isBootableImageType(image) &&
            (!image.properties || image.properties.image_type !== 'snapshot');
        }));
        addAllowedBootSource(model.images, SOURCE_TYPE_IMAGE, gettext('Image'));

        model.imageSnapshots.length = 0;
        push.apply(model.imageSnapshots, data.data.items.filter(function (image) {
          return isBootableImageType(image) &&
            (image.properties && image.properties.image_type === 'snapshot');
        }));

        addAllowedBootSource(
          model.imageSnapshots,
          SOURCE_TYPE_SNAPSHOT,
          gettext('Instance Snapshot')
        );
      }

      function getVolumes() {
        var volumePromises = [];
        // Need to check if Volume service is enabled before getting volumes
        model.volumeBootable = true;
        addAllowedBootSource(model.volumes, SOURCE_TYPE_VOLUME, gettext('Volume'));
        addAllowedBootSource(
          model.volumeSnapshots,
          SOURCE_TYPE_VOLUME_SNAPSHOT,
          gettext('Volume Snapshot')
        );
        volumePromises.push(cinderAPI.getVolumes({ status: 'available',  bootable: 1 })
          .then(onGetVolumes));
        volumePromises.push(cinderAPI.getVolumeSnapshots({ status: 'available' })
          .then(onGetVolumeSnapshots));

        // Can only boot image to volume if the Nova extension is enabled.
        novaExtensions.ifNameEnabled('BlockDeviceMappingV2Boot')
          .then(function() { model.allowCreateVolumeFromImage = true; });

        return $q.all(volumePromises);
      }

      function onGetVolumes(data) {
        model.volumes.length = 0;
        push.apply(model.volumes, data.data.items);
      }

      function onGetVolumeSnapshots(data) {
        model.volumeSnapshots.length = 0;
        push.apply(model.volumeSnapshots, data.data.items);
      }

      function addAllowedBootSource(rawTypes, type, label) {
        if (rawTypes && rawTypes.length > 0) {
          model.allowedBootSources.push({
            type: type,
            label: label
          });
        }
      }

      function setFinalSpecBootsource(finalSpec) {
        finalSpec.source_id = finalSpec.source && finalSpec.source[0] && finalSpec.source[0].id;
        delete finalSpec.source;

        switch (finalSpec.source_type.type) {
          case SOURCE_TYPE_IMAGE:
            setFinalSpecBootImageToVolume(finalSpec);
            break;
          case SOURCE_TYPE_SNAPSHOT:
            break;
          case SOURCE_TYPE_VOLUME:
            setFinalSpecBootFromVolumeDevice(finalSpec, 'vol');
            break;
          case SOURCE_TYPE_VOLUME_SNAPSHOT:
            setFinalSpecBootFromVolumeDevice(finalSpec, 'snap');
            break;
          default:
            $log.error("Unknown source type: " + finalSpec.source_type);
        }

        // The following are all fields gathered into simple fields by
        // steps so that the view can simply bind to simple model attributes
        // that are then transformed a single time to Nova's expectation
        // at launch time.
        delete finalSpec.source_type;
        delete finalSpec.vol_create;
        delete finalSpec.vol_device_name;
        delete finalSpec.vol_delete_on_terminate;
        delete finalSpec.vol_size;
      }

      function setFinalSpecBootImageToVolume(finalSpec) {
        if (finalSpec.vol_create) {
          // Specify null to get Autoselection (not empty string)
          var deviceName = finalSpec.vol_device_name ? finalSpec.vol_device_name : null;
          finalSpec.block_device_mapping_v2 = [];
          finalSpec.block_device_mapping_v2.push(
            {
              'device_name': deviceName,
              'source_type': SOURCE_TYPE_IMAGE,
              'destination_type': SOURCE_TYPE_VOLUME,
              'delete_on_termination': finalSpec.vol_delete_on_terminate ? 1 : 0,
              'uuid': finalSpec.source_id,
              'boot_index': '0',
              'volume_size': finalSpec.vol_size
            }
          );
          finalSpec.source_id = null;
        }
      }

      function setFinalSpecBootFromVolumeDevice(finalSpec, sourceType) {
        finalSpec.block_device_mapping = {};
        finalSpec.block_device_mapping[finalSpec.vol_device_name] = [
            finalSpec.source_id,
            ':',
            sourceType,
            '::',
            (finalSpec.vol_delete_on_terminate ? 1 : 0)
          ].join('');

        // Source ID must be empty for API
        finalSpec.source_id = '';
      }

      // Nova Limits

      function onGetNovaLimits(data) {
        angular.extend(model.novaLimits, data.data);
      }

      // Metadata Definitions

      /**
       * Metadata definitions provide supplemental information in detail
       * rows and should not slow down any of the other load processes.
       * All code should be written to treat metadata definitions as
       * optional, because they are never guaranteed to exist.
       */
      function getMetadataDefinitions() {
        // Metadata definitions often apply to multiple
        // resource types. It is optimal to make a single
        // request for all desired resource types.
        var resourceTypes = {
          flavor: 'OS::Nova::Flavor',
          image: 'OS::Glance::Image',
          volume: 'OS::Cinder::Volumes'
        };

        angular.forEach(resourceTypes, function (resourceType, key) {
          glanceAPI.getNamespaces({
            'resource_type': resourceType
          }, true)
          .then(function (data) {
            var namespaces = data.data.items;
            // This will ensure that the metaDefs model object remains
            // unchanged until metadefs are fully loaded. Otherwise,
            // partial results are loaded and can result in some odd
            // display behavior.
            if (namespaces.length) {
              model.metadataDefs[key] = namespaces;
            }
          });
        });
      }

      return model;
    }
  ]);

})();