'use strict';

const { Node, EdgeTriggeredInput } = require('../../../../noodl-runtime');

var Model = require('../../../model');
const CloudStore = require('../../../api/cloudstore');

var ModelNodeDefinition = {
  name: 'DbModel2',
  docs: 'https://docs.noodl.net/nodes/data/cloud-data/record',
  displayNodeName: 'Record',
  shortDesc: 'Database model',
  category: 'Cloud Services',
  usePortAsLabel: 'collectionName',
  color: 'data',
  dynamicports: [
    {
      name: 'conditionalports/extended',
      condition: 'idSource = explicit OR idSource NOT SET',
      inputs: ['modelId']
    }
  ],
  initialize: function () {
    var internal = this._internal;
    internal.inputValues = {};
    internal.relationModelIds = {};

    var _this = this;
    this._internal.onModelChangedCallback = function (args) {
      if (_this.isInputConnected('fetch')) return;

      if (_this.hasOutput('prop-' + args.name)) _this.flagOutputDirty('prop-' + args.name);

      if (_this.hasOutput('changed-' + args.name)) _this.sendSignalOnOutput('changed-' + args.name);

      _this.sendSignalOnOutput('changed');
    };
  },
  getInspectInfo() {
    const model = this._internal.model;
    if (!model) return '[No Record]';

    return [
      { type: 'text', value: 'Id: ' + model.getId() },
      { type: 'value', value: model.data }
    ];
  },
  outputs: {
    id: {
      type: 'string',
      displayName: 'Id',
      group: 'General',
      getter: function () {
        return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
      }
    },
    fetched: {
      type: 'signal',
      displayName: 'Fetched',
      group: 'Events'
    },
    changed: {
      type: 'signal',
      displayName: 'Changed',
      group: 'Events'
    },
    failure: {
      type: 'signal',
      displayName: 'Failure',
      group: 'Events'
    },
    error: {
      type: 'string',
      displayName: 'Error',
      group: 'Error',
      getter: function () {
        return this._internal.error;
      }
    }
  },
  inputs: {
    idSource: {
      type: {
        name: 'enum',
        enums: [
          { label: 'Specify explicitly', value: 'explicit' },
          { label: 'From repeater', value: 'foreach' }
        ],
        allowEditOnly: true
      },
      default: 'explicit',
      displayName: 'Id Source',
      group: 'General',
      set: function (value) {
        if (value === 'foreach') {
          this.scheduleAfterInputsHaveUpdated(() => {
            // Find closest nodescope that have a _forEachModel
            var component = this.nodeScope.componentOwner;
            while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
              component = component.parentNodeScope.componentOwner;
            }
            this.setModel(component !== undefined ? component._forEachModel : undefined);
          });
        }
      }
    },
    modelId: {
      type: { name: 'string', allowConnectionsOnly: true },
      displayName: 'Id',
      group: 'General',
      set: function (value) {
        if (value instanceof Model) value = value.getId();
        // Can be passed as model as well
        else if (typeof value === 'object') value = Model.create(value).getId(); // If this is an js object, dereference it

        this._internal.modelId = value; // Wait to fetch data
        if (this.isInputConnected('fetch') === false) this.setModelID(value);
        else {
          this.flagOutputDirty('id');
        }
      }
    },
    fetch: {
      displayName: 'Fetch',
      group: 'Actions',
      valueChangedToTrue: function () {
        this.scheduleFetch();
      }
    }
  },
  methods: {
    setCollectionID: function (id) {
      this._internal.collectionId = id;
    },
    setModelID: function (id) {
      var model = (this.nodeScope.modelScope || Model).get(id);
      // this._internal.modelIsNew = false;
      this.setModel(model);
    },
    setModel: function (model) {
      if (this._internal.model)
        // Remove old listener if existing
        this._internal.model.off('change', this._internal.onModelChangedCallback);

      this._internal.model = model;
      this.flagOutputDirty('id');
      model.on('change', this._internal.onModelChangedCallback);

      // We have a new model, mark all outputs as dirty
      for (var key in model.data) {
        if (this.hasOutput('prop-' + key)) this.flagOutputDirty('prop-' + key);
      }
      this.sendSignalOnOutput('fetched');
    },
    _onNodeDeleted: function () {
      Node.prototype._onNodeDeleted.call(this);
      if (this._internal.model) this._internal.model.off('change', this._internal.onModelChangedCallback);
    },
    scheduleOnce: function (type, cb) {
      const _this = this;
      const _type = 'hasScheduled' + type;
      if (this._internal[_type]) return;
      this._internal[_type] = true;
      this.scheduleAfterInputsHaveUpdated(function () {
        _this._internal[_type] = false;
        cb();
      });
    },
    setError: function (err) {
      this._internal.error = err;
      this.flagOutputDirty('error');
      this.sendSignalOnOutput('failure');

      if (this.context.editorConnection) {
        this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning', {
          message: err,
          showGlobally: true
        });
      }
    },
    clearWarnings() {
      if (this.context.editorConnection) {
        this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning');
      }
    },
    scheduleFetch: function () {
      var _this = this;
      const internal = this._internal;

      this.scheduleOnce('Fetch', function () {
        // Don't do fetch if no id
        if (internal.modelId === undefined || internal.modelId === '') {
          _this.setError('Missing Id.');
          return;
        }

        const cloudstore = CloudStore.forScope(_this.nodeScope.modelScope);
        cloudstore.fetch({
          collection: internal.collectionId,
          objectId: internal.modelId, // Get the objectId part of the model id
          success: function (response) {
            var model = cloudstore._fromJSON(response, internal.collectionId);
            if (internal.model !== model) {
              // Check if we need to change model
              if (internal.model)
                // Remove old listener if existing
                internal.model.off('change', internal.onModelChangedCallback);

              internal.model = model;
              model.on('change', internal.onModelChangedCallback);
            }
            _this.flagOutputDirty('id');

            delete response.objectId;

            for (var key in response) {
              if (_this.hasOutput('prop-' + key)) _this.flagOutputDirty('prop-' + key);
            }

            _this.sendSignalOnOutput('fetched');
          },
          error: function (err) {
            _this.setError(err || 'Failed to fetch.');
          }
        });
      });
    },
    scheduleStore: function () {
      var _this = this;
      var internal = this._internal;
      if (!internal.model) return;

      this.scheduleOnce('Store', function () {
        for (var i in internal.inputValues) {
          internal.model.set(i, internal.inputValues[i], { resolve: true });
        }
      });
    },
    registerOutputIfNeeded: function (name) {
      if (this.hasOutput(name)) {
        return;
      }

      if (name.startsWith('prop-'))
        this.registerOutput(name, {
          getter: userOutputGetter.bind(this, name.substring('prop-'.length))
        });
    },
    registerInputIfNeeded: function (name) {
      var _this = this;

      if (this.hasInput(name)) {
        return;
      }

      const dynamicSignals = {};

      if (dynamicSignals[name])
        return this.registerInput(name, {
          set: EdgeTriggeredInput.createSetter({
            valueChangedToTrue: dynamicSignals[name]
          })
        });

      const dynamicSetters = {
        collectionName: this.setCollectionID.bind(this)
      };

      if (dynamicSetters[name])
        return this.registerInput(name, {
          set: dynamicSetters[name]
        });

      if (name.startsWith('prop-'))
        this.registerInput(name, {
          set: userInputSetter.bind(this, name.substring('prop-'.length))
        });
    }
  }
};

function userOutputGetter(name) {
  /* jshint validthis:true */
  return this._internal.model ? this._internal.model.get(name, { resolve: true }) : undefined;
}

function userInputSetter(name, value) {
  //console.log('dbmodel setter:',name,value)
  /* jshint validthis:true */
  this._internal.inputValues[name] = value;
}

function updatePorts(nodeId, parameters, editorConnection, graphModel) {
  var ports = [];

  const dbCollections = graphModel.getMetaData('dbCollections');
  const systemCollections = graphModel.getMetaData('systemCollections');

  const _systemClasses = [
    { label: 'User', value: '_User' },
    { label: 'Role', value: '_Role' }
  ];
  ports.push({
    name: 'collectionName',
    displayName: 'Class',
    group: 'General',
    type: {
      name: 'enum',
      enums: _systemClasses.concat(
        dbCollections !== undefined
          ? dbCollections.map((c) => {
              return { value: c.name, label: c.name };
            })
          : []
      ),
      allowEditOnly: true
    },
    plug: 'input'
  });

  if (parameters.collectionName && dbCollections) {
    // Fetch ports from collection keys
    var c = dbCollections.find((c) => c.name === parameters.collectionName);
    if (c === undefined && systemCollections) c = systemCollections.find((c) => c.name === parameters.collectionName);
    if (c && c.schema && c.schema.properties) {
      var props = c.schema.properties;
      for (var key in props) {
        var p = props[key];
        if (ports.find((_p) => _p.name === key)) continue;

        if (p.type === 'Relation') {
        } else {
          // Other schema type ports
          const _typeMap = {
            String: 'string',
            Boolean: 'boolean',
            Number: 'number',
            Date: 'date'
          };

          ports.push({
            type: {
              name: _typeMap[p.type] ? _typeMap[p.type] : '*'
            },
            plug: 'output',
            group: 'Properties',
            name: 'prop-' + key,
            displayName: key
          });

          ports.push({
            type: 'signal',
            plug: 'output',
            group: 'Changed Events',
            displayName: key + ' Changed',
            name: 'changed-' + key
          });
        }
      }
    }
  }

  editorConnection.sendDynamicPorts(nodeId, ports);
}

module.exports = {
  node: ModelNodeDefinition,
  setup: function (context, graphModel) {
    if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
      return;
    }

    function _managePortsForNode(node) {
      updatePorts(node.id, node.parameters, context.editorConnection, graphModel);

      node.on('parameterUpdated', function (event) {
        updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
      });

      graphModel.on('metadataChanged.dbCollections', function (data) {
        CloudStore.invalidateCollections();
        updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
      });

      graphModel.on('metadataChanged.systemCollections', function (data) {
        CloudStore.invalidateCollections();
        updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
      });
    }

    graphModel.on('editorImportComplete', () => {
      graphModel.on('nodeAdded.DbModel2', function (node) {
        _managePortsForNode(node);
      });

      for (const node of graphModel.getNodesWithType('DbModel2')) {
        _managePortsForNode(node);
      }
    });
  }
};
