/**
@module @ember/application
*/

import { dictionary } from '@ember/-internals/utils';
import { get, findNamespace } from '@ember/-internals/metal';
import { assert, info } from '@ember/debug';
import { capitalize, classify, dasherize, decamelize } from '@ember/string';
import { Object as EmberObject } from '@ember/-internals/runtime';
import validateType from './lib/validate-type';
import { getTemplate } from '@ember/-internals/glimmer';
import { DEBUG } from '@glimmer/env';

/**
  The DefaultResolver defines the default lookup rules to resolve
  container lookups before consulting the container for registered
  items:

  * templates are looked up on `Ember.TEMPLATES`
  * other names are looked up on the application after converting
    the name. For example, `controller:post` looks up
    `App.PostController` by default.
  * there are some nuances (see examples below)

  ### How Resolving Works

  The container calls this object's `resolve` method with the
  `fullName` argument.

  It first parses the fullName into an object using `parseName`.

  Then it checks for the presence of a type-specific instance
  method of the form `resolve[Type]` and calls it if it exists.
  For example if it was resolving 'template:post', it would call
  the `resolveTemplate` method.

  Its last resort is to call the `resolveOther` method.

  The methods of this object are designed to be easy to override
  in a subclass. For example, you could enhance how a template
  is resolved like so:

  ```app/app.js
  import Application from '@ember/application';
  import GlobalsResolver from '@ember/application/globals-resolver';

  App = Application.create({
    Resolver: GlobalsResolver.extend({
      resolveTemplate(parsedName) {
        let resolvedTemplate = this._super(parsedName);
        if (resolvedTemplate) { return resolvedTemplate; }

        return Ember.TEMPLATES['not_found'];
      }
    })
  });
  ```

  Some examples of how names are resolved:

  ```text
  'template:post'           //=> Ember.TEMPLATES['post']
  'template:posts/byline'   //=> Ember.TEMPLATES['posts/byline']
  'template:posts.byline'   //=> Ember.TEMPLATES['posts/byline']
  'template:blogPost'       //=> Ember.TEMPLATES['blog-post']
  'controller:post'         //=> App.PostController
  'controller:posts.index'  //=> App.PostsIndexController
  'controller:blog/post'    //=> Blog.PostController
  'controller:basic'        //=> Controller
  'route:post'              //=> App.PostRoute
  'route:posts.index'       //=> App.PostsIndexRoute
  'route:blog/post'         //=> Blog.PostRoute
  'route:basic'             //=> Route
  'foo:post'                //=> App.PostFoo
  'model:post'              //=> App.Post
  ```

  @class GlobalsResolver
  @extends EmberObject
  @public
*/

class DefaultResolver extends EmberObject {
  static create(props) {
    // DO NOT REMOVE even though this doesn't do anything
    // This is required for a FireFox 60+ JIT bug with our tests.
    // without it, create(props) in our tests would lose props on a deopt.
    return super.create(props);
  }

  /**
    This will be set to the Application instance when it is
    created.

    @property namespace
    @public
  */

  init() {
    this._parseNameCache = dictionary(null);
  }

  normalize(fullName) {
    let [type, name] = fullName.split(':');

    assert(
      'Tried to normalize a container name without a colon (:) in it. ' +
        'You probably tried to lookup a name that did not contain a type, ' +
        'a colon, and a name. A proper lookup name would be `view:post`.',
      fullName.split(':').length === 2
    );

    if (type !== 'template') {
      let result = name.replace(/(\.|_|-)./g, m => m.charAt(1).toUpperCase());

      return `${type}:${result}`;
    } else {
      return fullName;
    }
  }

  /**
    This method is called via the container's resolver method.
    It parses the provided `fullName` and then looks up and
    returns the appropriate template or class.

    @method resolve
    @param {String} fullName the lookup string
    @return {Object} the resolved factory
    @public
  */
  resolve(fullName) {
    let parsedName = this.parseName(fullName);
    let resolveMethodName = parsedName.resolveMethodName;
    let resolved;

    if (this[resolveMethodName]) {
      resolved = this[resolveMethodName](parsedName);
    }

    resolved = resolved || this.resolveOther(parsedName);

    if (DEBUG) {
      if (parsedName.root && parsedName.root.LOG_RESOLVER) {
        this._logLookup(resolved, parsedName);
      }
    }

    if (resolved) {
      validateType(resolved, parsedName);
    }

    return resolved;
  }

  /**
    Convert the string name of the form 'type:name' to
    a Javascript object with the parsed aspects of the name
    broken out.

    @param {String} fullName the lookup string
    @method parseName
    @protected
  */

  parseName(fullName) {
    return (
      this._parseNameCache[fullName] || (this._parseNameCache[fullName] = this._parseName(fullName))
    );
  }

  _parseName(fullName) {
    let [type, fullNameWithoutType] = fullName.split(':');

    let name = fullNameWithoutType;
    let namespace = get(this, 'namespace');
    let root = namespace;
    let lastSlashIndex = name.lastIndexOf('/');
    let dirname = lastSlashIndex !== -1 ? name.slice(0, lastSlashIndex) : null;

    if (type !== 'template' && lastSlashIndex !== -1) {
      let parts = name.split('/');
      name = parts[parts.length - 1];
      let namespaceName = capitalize(parts.slice(0, -1).join('.'));
      root = findNamespace(namespaceName);

      assert(
        `You are looking for a ${name} ${type} in the ${namespaceName} namespace, but the namespace could not be found`,
        root
      );
    }

    let resolveMethodName = fullNameWithoutType === 'main' ? 'Main' : classify(type);

    if (!(name && type)) {
      throw new TypeError(`Invalid fullName: \`${fullName}\`, must be of the form \`type:name\` `);
    }

    return {
      fullName,
      type,
      fullNameWithoutType,
      dirname,
      name,
      root,
      resolveMethodName: `resolve${resolveMethodName}`,
    };
  }

  /**
    Returns a human-readable description for a fullName. Used by the
    Application namespace in assertions to describe the
    precise name of the class that Ember is looking for, rather than
    container keys.

    @param {String} fullName the lookup string
    @method lookupDescription
    @protected
  */
  lookupDescription(fullName) {
    let parsedName = this.parseName(fullName);
    let description;

    if (parsedName.type === 'template') {
      return `template at ${parsedName.fullNameWithoutType.replace(/\./g, '/')}`;
    }

    description = `${parsedName.root}.${classify(parsedName.name).replace(/\./g, '')}`;

    if (parsedName.type !== 'model') {
      description += classify(parsedName.type);
    }

    return description;
  }

  makeToString(factory) {
    return factory.toString();
  }

  /**
    Given a parseName object (output from `parseName`), apply
    the conventions expected by `Router`

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method useRouterNaming
    @protected
  */
  useRouterNaming(parsedName) {
    if (parsedName.name === 'basic') {
      parsedName.name = '';
    } else {
      parsedName.name = parsedName.name.replace(/\./g, '_');
    }
  }
  /**
    Look up the template in Ember.TEMPLATES

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveTemplate
    @protected
  */
  resolveTemplate(parsedName) {
    let templateName = parsedName.fullNameWithoutType.replace(/\./g, '/');

    return getTemplate(templateName) || getTemplate(decamelize(templateName));
  }

  /**
    Lookup the view using `resolveOther`

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveView
    @protected
  */
  resolveView(parsedName) {
    this.useRouterNaming(parsedName);
    return this.resolveOther(parsedName);
  }

  /**
    Lookup the controller using `resolveOther`

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveController
    @protected
  */
  resolveController(parsedName) {
    this.useRouterNaming(parsedName);
    return this.resolveOther(parsedName);
  }
  /**
    Lookup the route using `resolveOther`

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveRoute
    @protected
  */
  resolveRoute(parsedName) {
    this.useRouterNaming(parsedName);
    return this.resolveOther(parsedName);
  }

  /**
    Lookup the model on the Application namespace

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveModel
    @protected
  */
  resolveModel(parsedName) {
    let className = classify(parsedName.name);
    let factory = get(parsedName.root, className);

    return factory;
  }
  /**
    Look up the specified object (from parsedName) on the appropriate
    namespace (usually on the Application)

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveHelper
    @protected
  */
  resolveHelper(parsedName) {
    return this.resolveOther(parsedName);
  }
  /**
    Look up the specified object (from parsedName) on the appropriate
    namespace (usually on the Application)

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveOther
    @protected
  */
  resolveOther(parsedName) {
    let className = classify(parsedName.name) + classify(parsedName.type);
    let factory = get(parsedName.root, className);
    return factory;
  }

  resolveMain(parsedName) {
    let className = classify(parsedName.type);
    return get(parsedName.root, className);
  }

  /**
    Used to iterate all items of a given type.

    @method knownForType
    @param {String} type the type to search for
    @private
  */
  knownForType(type) {
    let namespace = get(this, 'namespace');
    let suffix = classify(type);
    let typeRegexp = new RegExp(`${suffix}$`);

    let known = dictionary(null);
    let knownKeys = Object.keys(namespace);
    for (let index = 0; index < knownKeys.length; index++) {
      let name = knownKeys[index];

      if (typeRegexp.test(name)) {
        let containerName = this.translateToContainerFullname(type, name);

        known[containerName] = true;
      }
    }

    return known;
  }

  /**
    Converts provided name from the backing namespace into a container lookup name.

    Examples:

    * App.FooBarHelper -> helper:foo-bar
    * App.THelper -> helper:t

    @method translateToContainerFullname
    @param {String} type
    @param {String} name
    @private
  */
  translateToContainerFullname(type, name) {
    let suffix = classify(type);
    let namePrefix = name.slice(0, suffix.length * -1);
    let dasherizedName = dasherize(namePrefix);

    return `${type}:${dasherizedName}`;
  }
}

export default DefaultResolver;

if (DEBUG) {
  /**
      @method _logLookup
      @param {Boolean} found
      @param {Object} parsedName
      @private
    */
  DefaultResolver.prototype._logLookup = function(found, parsedName) {
    let symbol = found ? '[✓]' : '[ ]';

    let padding;
    if (parsedName.fullName.length > 60) {
      padding = '.';
    } else {
      padding = new Array(60 - parsedName.fullName.length).join('.');
    }

    info(symbol, parsedName.fullName, padding, this.lookupDescription(parsedName.fullName));
  };
}