import Mixin from '@ember/object/mixin';
import { camelize, pluralize } from '@ember-data/request-utils/string';

/*
 The structure of this file is such because typing Mixins is hard. Here we've structured it in
 such a way as to maximize the type information that a consumer can utilize. There are simpler
 ways to type a mixin but we would not be able to provide the nice overload signature for buildURL
*/
// the interface must fully declare the function signatures that the individual functions
// will also declare. If instead we try to keep them in sync by doing something like
// `interface BuildURLMixin { buildURL: typeof buildURL }`
// then an extending class overwriting one of the methods will break because typescript
// thinks it is a switch from an instance prop (that is a method) to an instance method.

// prevents the final constructed object from needing to add
// host and namespace which are provided by the final consuming
// class to the prototype which can result in overwrite errors

/**
  ## Using BuildURLMixin

  To use URL building, include the mixin when extending an adapter, and call `buildURL` where needed.
  The default behaviour is designed for RESTAdapter.

  ### Example

  ```javascript
  import Adapter, { BuildURLMixin } from '@ember-data/adapter';

  export default class ApplicationAdapter extends Adapter.extend(BuildURLMixin) {
    findRecord(store, type, id, snapshot) {
      var url = this.buildURL(type.modelName, id, snapshot, 'findRecord');
      return this.ajax(url, 'GET');
    }
  }
  ```

  ### Attributes

  The `host` and `namespace` attributes will be used if defined, and are optional.

  @class BuildURLMixin
  @public
*/
/**
    Builds a URL for a given type and optional ID.

    By default, it pluralizes the type's name (for example, 'post'
    becomes 'posts' and 'person' becomes 'people'). To override the
    pluralization see [pathForType](./pathForType?anchor=pathForType).

    If an ID is specified, it adds the ID to the path generated
    for the type, separated by a `/`.

    When called by `RESTAdapter.findMany()` the `id` and `snapshot` parameters
    will be arrays of ids and snapshots.

    @public
    @param {String} modelName
    @param {(String|Array|Object)} id single id or array of ids or query
    @param {(Snapshot|SnapshotRecordArray)} snapshot single snapshot or array of snapshots
    @param {String} requestType
    @param {Object} query object of query parameters to send for query requests.
    @return {String} url
  */

function buildURL(modelName, id, snapshot, requestType, query) {
  /*
      Switch statements in typescript don't currently narrow even when the function is implemented
      with overloads.
       We still extract this to stand alone so that we can provide nice overloads for calling signatures,
      but we will still require all of this casting (or a ridiculous number of assertsthat narrow it down
      for us).
  */
  switch (requestType) {
    case 'findRecord':
      return this.urlForFindRecord(id, modelName, snapshot);
    case 'findAll':
      return this.urlForFindAll(modelName, snapshot);
    case 'query':
      return this.urlForQuery(query || {}, modelName);
    case 'queryRecord':
      return this.urlForQueryRecord(query || {}, modelName);
    case 'findMany':
      return this.urlForFindMany(id, modelName, snapshot);
    case 'findHasMany':
      return this.urlForFindHasMany(id, modelName, snapshot);
    case 'findBelongsTo':
      return this.urlForFindBelongsTo(id, modelName, snapshot);
    case 'createRecord':
      return this.urlForCreateRecord(modelName, snapshot);
    case 'updateRecord':
      return this.urlForUpdateRecord(id, modelName, snapshot);
    case 'deleteRecord':
      return this.urlForDeleteRecord(id, modelName, snapshot);
    default:
      // this is the 'never' case but someone may call `buildURL` manually so
      // we try to do something for them.
      return this._buildURL(modelName, id);
  }
}

/**
    @private
    @param {String} modelName
    @param {String} id
    @return {String} url
  */
function _buildURL(modelName, id) {
  let path;
  const url = [];
  const {
    host
  } = this;
  const prefix = this.urlPrefix();
  if (modelName) {
    path = this.pathForType(modelName);
    if (path) {
      url.push(path);
    }
  }
  if (id) {
    url.push(encodeURIComponent(id));
  }
  if (prefix) {
    url.unshift(prefix);
  }
  let urlString = url.join('/');
  if (!host && urlString && urlString.charAt(0) !== '/') {
    urlString = '/' + urlString;
  }
  return urlString;
}

/**
   Builds a URL for a `store.findRecord(type, id)` call.

   Example:

   ```js [app/adapters/user.js]
   import JSONAPIAdapter from '@ember-data/adapter/json-api';

   export default class ApplicationAdapter extends JSONAPIAdapter {
     urlForFindRecord(id, modelName, snapshot) {
       let baseUrl = this.buildURL(modelName, id, snapshot);
       return `${baseUrl}/users/${snapshot.adapterOptions.user_id}/playlists/${id}`;
     }
   }
   ```

   @public
   @param {String} id
   @param {String} modelName
   @param {Snapshot} snapshot
   @return {String} url

   */
function urlForFindRecord(id, modelName, snapshot) {
  return this._buildURL(modelName, id);
}

/**
   Builds a URL for a `store.findAll(type)` call.

   Example:

   ```js [app/adapters/comment.js]
   import JSONAPIAdapter from '@ember-data/adapter/json-api';

   export default class ApplicationAdapter extends JSONAPIAdapter {
     urlForFindAll(modelName, snapshot) {
       let baseUrl = this.buildURL(modelName);
       return `${baseUrl}/data/comments.json`;
     }
   }
   ```

    @public
   @param {String} modelName
   @param {SnapshotRecordArray} snapshot
   @return {String} url
   */
function urlForFindAll(modelName, snapshots) {
  return this._buildURL(modelName);
}

/**
   Builds a URL for a `store.query(type, query)` call.

   Example:

   ```js [app/adapters/application.js]
   import RESTAdapter from '@ember-data/adapter/rest';

   export default class ApplicationAdapter extends RESTAdapter {
     host = 'https://api.github.com';
     urlForQuery (query, modelName) {
       switch(modelName) {
         case 'repo':
           return `https://api.github.com/orgs/${query.orgId}/repos`;
         default:
           return super.urlForQuery(...arguments);
       }
     }
   }
   ```

    @public
   @param {Object} query
   @param {String} modelName
   @return {String} url
   */
function urlForQuery(query, modelName) {
  return this._buildURL(modelName);
}

/**
   Builds a URL for a `store.queryRecord(type, query)` call.

   Example:

   ```js [app/adapters/application.js]
   import RESTAdapter from '@ember-data/adapter/rest';

   export default class ApplicationAdapter extends RESTAdapter {
     urlForQueryRecord({ slug }, modelName) {
       let baseUrl = this.buildURL();
       return `${baseUrl}/${encodeURIComponent(slug)}`;
     }
   }
   ```

    @public
   @param {Object} query
   @param {String} modelName
   @return {String} url
   */
function urlForQueryRecord(query, modelName) {
  return this._buildURL(modelName);
}

/**
   Builds a URL for coalescing multiple `store.findRecord(type, id)`
   records into 1 request when the adapter's `coalesceFindRequests`
   property is `true`.

   Example:

   ```js [app/adapters/application.js]
   import RESTAdapter from '@ember-data/adapter/rest';

   export default class ApplicationAdapter extends RESTAdapter {
     urlForFindMany(ids, modelName) {
       let baseUrl = this.buildURL();
       return `${baseUrl}/coalesce`;
     }
   }
   ```

    @public
   @param {Array} ids
   @param {String} modelName
   @param {Array} snapshots
   @return {String} url
   */
function urlForFindMany(ids, modelName, snapshots) {
  return this._buildURL(modelName);
}

/**
   Builds a URL for fetching an async `hasMany` relationship when a URL
   is not provided by the server.

   Example:

   ```js [app/adapters/application.js]
   import JSONAPIAdapter from '@ember-data/adapter/json-api';

   export default class ApplicationAdapter extends JSONAPIAdapter {
     urlForFindHasMany(id, modelName, snapshot) {
       let baseUrl = this.buildURL(modelName, id);
       return `${baseUrl}/relationships`;
     }
   }
   ```

    @public
   @param {String} id
   @param {String} modelName
   @param {Snapshot} snapshot
   @return {String} url
   */
function urlForFindHasMany(id, modelName, snapshot) {
  return this._buildURL(modelName, id);
}

/**
   Builds a URL for fetching an async `belongsTo` relationship when a url
   is not provided by the server.

   Example:

   ```js [app/adapters/application.js]
   import JSONAPIAdapter from '@ember-data/adapter/json-api';

   export default class ApplicationAdapter extends JSONAPIAdapter {
     urlForFindBelongsTo(id, modelName, snapshot) {
       let baseUrl = this.buildURL(modelName, id);
       return `${baseUrl}/relationships`;
     }
   }
   ```

    @public
   @param {String} id
   @param {String} modelName
   @param {Snapshot} snapshot
   @return {String} url
   */
function urlForFindBelongsTo(id, modelName, snapshot) {
  return this._buildURL(modelName, id);
}

/**
   Builds a URL for a `record.save()` call when the record was created
   locally using `store.createRecord()`.

   Example:

   ```js [app/adapters/application.js]
   import RESTAdapter from '@ember-data/adapter/rest';

   export default class ApplicationAdapter extends RESTAdapter {
     urlForCreateRecord(modelName, snapshot) {
       return super.urlForCreateRecord(...arguments) + '/new';
     }
   }
   ```

    @public
   @param {String} modelName
   @param {Snapshot} snapshot
   @return {String} url
   */
function urlForCreateRecord(modelName, snapshot) {
  return this._buildURL(modelName);
}

/**
   Builds a URL for a `record.save()` call when the record has been updated locally.

   Example:

   ```js [app/adapters/application.js]
   import RESTAdapter from '@ember-data/adapter/rest';

   export default class ApplicationAdapter extends RESTAdapter {
     urlForUpdateRecord(id, modelName, snapshot) {
       return `/${id}/feed?access_token=${snapshot.adapterOptions.token}`;
     }
   }
   ```

    @public
   @param {String} id
   @param {String} modelName
   @param {Snapshot} snapshot
   @return {String} url
   */
function urlForUpdateRecord(id, modelName, snapshot) {
  return this._buildURL(modelName, id);
}

/**
   Builds a URL for a `record.save()` call when the record has been deleted locally.

   Example:

   ```js [app/adapters/application.js]
   import RESTAdapter from '@ember-data/adapter/rest';

   export default class ApplicationAdapter extends RESTAdapter {
     urlForDeleteRecord(id, modelName, snapshot) {
       return super.urlForDeleteRecord(...arguments) + '/destroy';
     }
   }
   ```

    @public
   @param {String} id
   @param {String} modelName
   @param {Snapshot} snapshot
   @return {String} url
   */
function urlForDeleteRecord(id, modelName, snapshot) {
  return this._buildURL(modelName, id);
}

/**
    @private
    @param {String} path
    @param {String} parentURL
    @return {String} urlPrefix
  */
function urlPrefix(path, parentURL) {
  const {
    namespace
  } = this;
  let {
    host
  } = this;
  if (!host || host === '/') {
    host = '';
  }
  if (path) {
    // Protocol relative url
    if (/^\/\//.test(path) || /http(s)?:\/\//.test(path)) {
      // Do nothing, the full host is already included.
      return path;

      // Absolute path
    } else if (path.charAt(0) === '/') {
      return `${host}${path}`;
      // Relative path
    } else {
      return `${parentURL}/${path}`;
    }
  }

  // No path provided
  const url = [];
  if (host) {
    url.push(host);
  }
  if (namespace) {
    url.push(namespace);
  }
  return url.join('/');
}

/**
    Determines the pathname for a given type.

    By default, it pluralizes the type's name (for example,
    'post' becomes 'posts' and 'person' becomes 'people').

    ### Pathname customization

    For example, if you have an object `LineItem` with an
    endpoint of `/line_items/`.

    ```js [app/adapters/application.js]
    import RESTAdapter from '@ember-data/adapter/rest';
    import { undesrcore, pluralize } from '<app-name>/utils/string-utils';

    export default class ApplicationAdapter extends RESTAdapter {
      pathForType(modelName) {
        return pluralize(underscore(modelName));
      }
    }
    ```

    @public
    @param {String} modelName
    @return {String} path
  **/
function pathForType(modelName) {
  const camelized = camelize(modelName);
  return pluralize(camelized);
}

// we build it this way vs casting to BuildURLMixin so that any
// changes to the interface surface as errors here.
const mixinProps = {
  buildURL,
  _buildURL,
  urlForFindRecord,
  urlForFindAll,
  urlForQueryRecord,
  urlForQuery,
  urlForFindMany,
  urlForFindHasMany,
  urlForFindBelongsTo,
  urlForCreateRecord,
  urlForDeleteRecord,
  urlForUpdateRecord,
  urlPrefix,
  pathForType
};
const BuildURLMixin = Mixin.create(mixinProps);
export { BuildURLMixin as B };