import { A } from '@ember/array';
import DataAdapter from '@ember/debug/data-adapter';
import { addObserver, removeObserver } from '@ember/object/observers';
import * as s from '@ember/service';
import { macroCondition, getGlobalConfig } from '@embroider/macros';
import { capitalize, underscore } from '@ember-data-mirror/request-utils/string';
import { recordIdentifierFor } from '@ember-data-mirror/store';

const deferred = /* @__PURE__ */new WeakMap();
function deferDecorator(proto, prop, desc) {
  let map = deferred.get(proto);
  if (!map) {
    map = /* @__PURE__ */new Map();
    deferred.set(proto, map);
  }
  map.set(prop, desc);
}
function findDeferredDecorator(target, prop) {
  var _a;
  let cursor = target.prototype;
  while (cursor) {
    let desc = (_a = deferred.get(cursor)) == null ? void 0 : _a.get(prop);
    if (desc) {
      return desc;
    }
    cursor = cursor.prototype;
  }
}
function decorateFieldV2(prototype, prop, decorators, initializer) {
  let desc = {
    configurable: true,
    enumerable: true,
    writable: true,
    initializer: null
  };
  if (initializer) {
    desc.initializer = initializer;
  }
  for (let decorator of decorators) {
    desc = decorator(prototype, prop, desc) || desc;
  }
  if (desc.initializer === void 0) {
    Object.defineProperty(prototype, prop, desc);
  } else {
    deferDecorator(prototype, prop, desc);
  }
}
function initializeDeferredDecorator(target, prop) {
  let desc = findDeferredDecorator(target.constructor, prop);
  if (desc) {
    Object.defineProperty(target, prop, {
      enumerable: desc.enumerable,
      configurable: desc.configurable,
      writable: desc.writable,
      value: desc.initializer ? desc.initializer.call(target) : void 0
    });
  }
}

const service = s.service ?? s.inject;
const StoreTypesMap = new WeakMap();
function debugInfo() {
  const relationships = {};
  const expensiveProperties = [];
  const identifier = recordIdentifierFor(this);
  const fields = this.store.schema.fields(identifier);
  const attrGroup = {
    name: 'Attributes',
    properties: ['id'],
    expand: true
  };
  const attributes = attrGroup.properties;
  const groups = [attrGroup];
  for (const field of fields.values()) {
    switch (field.kind) {
      case 'attribute':
        attributes.push(field.name);
        break;
      case 'belongsTo':
      case 'hasMany':
        {
          let properties = relationships[field.kind];
          if (properties === undefined) {
            properties = relationships[field.kind] = [];
            groups.push({
              name: field.kind,
              properties,
              expand: true
            });
          }
          properties.push(field.name);
          expensiveProperties.push(field.name);
          break;
        }
    }
  }
  groups.push({
    name: 'Flags',
    properties: ['isLoaded', 'hasDirtyAttributes', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid'],
    expand: false
  });
  return {
    propertyInfo: {
      // include all other mixins / properties (not just the grouped ones)
      includeOtherProperties: true,
      groups: groups,
      // don't pre-calculate unless cached
      expensiveProperties: expensiveProperties
    }
  };
}
function installDebugInfo(ModelKlass) {
  /**
   Provides info about the model for debugging purposes
   by grouping the properties into more semantic groups.
    Meant to be used by debugging tools such as the Chrome Ember Extension.
    - Groups all attributes in "Attributes" group.
   - Groups all belongsTo relationships in "Belongs To" group.
   - Groups all hasMany relationships in "Has Many" group.
   - Groups all flags in "Flags" group.
   - Flags relationship CPs as expensive properties.
    @internal
   */
  ModelKlass.prototype._debugInfo = debugInfo;
}
function typesMapFor(store) {
  let typesMap = StoreTypesMap.get(store);
  if (typesMap === undefined) {
    typesMap = new Map();
    StoreTypesMap.set(store, typesMap);
  }
  return typesMap;
}

/**
  Implements `@ember/debug/data-adapter` with for EmberData
  integration with the ember-inspector.

  @private
*/
class InspectorDataAdapter extends DataAdapter {
  static {
    decorateFieldV2(this.prototype, "store", [service('store')]);
  }
  #store = (initializeDeferredDecorator(this, "store"), void 0);
  /**
    Specifies how records can be filtered based on the state of the record
    Records returned will need to have a `filterValues`
    property with a key for every name in the returned array
     @private
    @return List of objects defining filters
     The object should have a `name` and `desc` property
  */
  getFilters() {
    return [{
      name: 'isNew',
      desc: 'New'
    }, {
      name: 'isModified',
      desc: 'Modified'
    }, {
      name: 'isClean',
      desc: 'Clean'
    }];
  }
  _nameToClass(type) {
    return this.store.modelFor(type);
  }

  /**
    Fetch the model types and observe them for changes.
    Maintains the list of model types without needing the Model package for detection.
     @private
    @param typesAdded Callback to call to add types.
    Takes an array of objects containing wrapped types (returned from `wrapModelType`).
    @param typesUpdated Callback to call when a type has changed.
    Takes an array of objects containing wrapped types.
    @return Method to call to remove all observers
  */
  watchModelTypes(typesAdded, typesUpdated) {
    const {
      store
    } = this;
    const discoveredTypes = typesMapFor(store);
    const unsub = store.notifications.subscribe('resource', (identifier, notificationType) => {
      if (notificationType === 'added') {
        this.watchTypeIfUnseen(store, discoveredTypes, identifier.type, typesAdded, typesUpdated, _releaseMethods);
      }
    });
    const _releaseMethods = [() => {
      store.notifications.unsubscribe(unsub);
    }];
    Object.keys(store.identifierCache._cache.resourcesByType).forEach(type => {
      discoveredTypes.set(type, false);
    });

    // Add any models that were added during initialization of the app, before the inspector was opened
    discoveredTypes.forEach((_, type) => {
      this.watchTypeIfUnseen(store, discoveredTypes, type, typesAdded, typesUpdated, _releaseMethods);
    });
    const release = () => {
      _releaseMethods.forEach(fn => fn());
      // reset the list so the models can be added if the inspector is re-opened
      // the entries are set to false instead of removed, since the models still exist in the app
      // we just need the inspector to become aware of them
      discoveredTypes.forEach((value, key) => {
        discoveredTypes.set(key, false);
      });
      this.releaseMethods.removeObject(release);
    };
    this.releaseMethods.pushObject(release);
    return release;
  }

  /**
   * Loop over the discovered types and use the callbacks from watchModelTypes to notify
   * the consumer of this adapter about the mdoels.
   *
   * @private
   */
  watchTypeIfUnseen(store, discoveredTypes, type, typesAdded, typesUpdated, releaseMethods) {
    if (discoveredTypes.get(type) !== true) {
      const klass = store.modelFor(type);
      installDebugInfo(klass);
      const wrapped = this.wrapModelType(klass, type);
      releaseMethods.push(this.observeModelType(type, typesUpdated));
      typesAdded([wrapped]);
      discoveredTypes.set(type, true);
    }
  }

  /**
    Creates a human readable string used for column headers
     @private
    @param name The attribute name
    @return Human readable string based on the attribute name
  */
  columnNameToDesc(name) {
    return capitalize(underscore(name).replace(/_/g, ' ').trim());
  }

  /**
    Get the columns for a given model type
     @private
    @return An array of columns of the following format:
     name: {String} The name of the column
     desc: {String} Humanized description (what would show in a table column name)
  */
  columnsForType(typeClass) {
    const columns = [{
      name: 'id',
      desc: 'Id'
    }];
    let count = 0;
    typeClass.attributes.forEach((meta, name) => {
      if (count++ > this.attributeLimit) {
        return false;
      }
      const desc = this.columnNameToDesc(name);
      columns.push({
        name: name,
        desc: desc
      });
    });
    return columns;
  }

  /**
    Fetches all loaded records for a given type
     @private
    @param modelClass of the record
    @param modelName of the record
    @return An array of Model records
     This array will be observed for changes,
     so it should update when new records are added/removed
  */
  getRecords(modelClass, modelName) {
    if (arguments.length < 2) {
      // Legacy Ember.js < 1.13 support
      const containerKey = modelClass._debugContainerKey;
      if (containerKey) {
        const match = containerKey.match(/model:(.*)/);
        if (match !== null) {
          modelName = match[1];
        }
      }
    }
    macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
      if (!test) {
        throw new Error('Cannot find model name. Please upgrade to Ember.js >= 1.13 for Ember Inspector support');
      }
    })(!!modelName) : {};
    return this.store.peekAll(modelName);
  }

  /**
    Gets the values for each column
    This is the attribute values for a given record
     @private
    @param record to get values from
    @return Keys should match column names defined by the model type
  */
  getRecordColumnValues(record) {
    let count = 0;
    const columnValues = {
      id: record.id
    };
    record.eachAttribute(key => {
      if (count++ > this.attributeLimit) {
        return false;
      }
      columnValues[key] = record[key];
    });
    return columnValues;
  }

  /**
    Returns keywords to match when searching records
     @private
    @return Relevant keywords for search based on the record's attribute values
  */
  getRecordKeywords(record) {
    const keywords = [record.id];
    record.eachAttribute(key => {
      keywords.push(record[key]);
    });
    return A(keywords);
  }

  /**
    Returns the values of filters defined by `getFilters`
    These reflect the state of the record
     @private
    @return The record state filter values
  */
  getRecordFilterValues(record) {
    return {
      isNew: record.isNew,
      isModified: record.hasDirtyAttributes && !record.isNew,
      isClean: !record.hasDirtyAttributes
    };
  }

  /**
    Returns a color that represents the record's state
    Possible colors: black, blue, green
     @private
    @return The record color
  */
  getRecordColor(record) {
    let color = 'black';
    if (record.isNew) {
      color = 'green';
    } else if (record.hasDirtyAttributes) {
      color = 'blue';
    }
    return color;
  }

  /**
    Observes all relevant properties and re-sends the wrapped record
    when a change occurs
     @private
    @param record
    @param recordUpdated Callback used to notify changes
    @return The function to call to remove all observers
  */
  observeRecord(record, recordUpdated) {
    const releaseMethods = [];
    const keysToObserve = ['id', 'isNew', 'hasDirtyAttributes'];
    record.eachAttribute(key => keysToObserve.push(key));
    keysToObserve.forEach(key => {
      const handler = () => {
        recordUpdated(this.wrapRecord(record));
      };
      addObserver(record, key, handler);
      releaseMethods.push(function () {
        removeObserver(record, key, handler);
      });
    });
    const release = function () {
      releaseMethods.forEach(fn => fn());
    };
    return release;
  }
}
const Exported = macroCondition(getGlobalConfig().WarpDriveMirror.includeDataAdapter) ? InspectorDataAdapter : null;

export { Exported as default };
