const { Message } = require('../message-type');
const Interface = require('./interface').Interface;
const assertObjectPathValid = require('../validators').assertObjectPathValid;

class ServiceObject {
  constructor (path, bus) {
    assertObjectPathValid(path);
    this.path = path;
    this.bus = bus;
    this.interfaces = {};
    this._handlers = {};
  }

  addInterface (iface) {
    if (!(iface instanceof Interface)) {
      throw new Error(`object.addInterface takes an Interface as the first argument (got ${iface})`);
    }
    if (this.interfaces[iface.$name]) {
      throw new Error(`an interface with name '${iface.$name}' is already exported on this object`);
    }
    this.interfaces[iface.$name] = iface;
    const that = this;

    const propertiesChangedHandler = function (changedProperties, invalidatedProperties) {
      const body = [
        iface.$name,
        changedProperties,
        invalidatedProperties
      ];
      that.bus.send(Message.newSignal(that.path, 'org.freedesktop.DBus.Properties', 'PropertiesChanged', 'sa{sv}as', body));
    };

    const signalHandler = function (options, result) {
      // TODO lots of repeated code with the method handler here
      const {
        signature,
        signatureTree,
        name
      } = options;
      if (result === undefined) {
        result = [];
      } else if (signatureTree.length === 1) {
        result = [result];
      } else if (!Array.isArray(result)) {
        throw new Error(`signal ${iface.$name}.${name} expected to return multiple arguments in an array (signature: '${signature}')`);
      }

      if (signatureTree.length !== result.length) {
        throw new Error(`signal ${iface.$name}.${name} returned the wrong number of arguments (got ${result.length} expected ${signatureTree.length}) for signature '${signature}'`);
      }

      that.bus.send(Message.newSignal(that.path, iface.$name, name, signature, result));
    };

    this._handlers[iface.$name] = {
      propertiesChanged: propertiesChangedHandler,
      signal: signalHandler
    };

    iface.$emitter.on('signal', signalHandler);
    iface.$emitter.on('properties-changed', propertiesChangedHandler);
  }

  removeInterface (iface) {
    if (!(iface instanceof Interface)) {
      throw new Error(`object.removeInterface takes an Interface as the first argument (got ${iface})`);
    }
    if (!this.interfaces[iface.$name]) {
      throw new Error(`Interface ${iface.$name} not exported on this object`);
    }
    const handlers = this._handlers[iface.$name];
    iface.$emitter.removeListener('signal', handlers.signal);
    iface.$emitter.removeListener('properties-changed', handlers.propertiesChanged);
    delete this._handlers[iface.$name];
    delete this.interfaces[iface.$name];
  }

  introspect () {
    const interfaces = ServiceObject.defaultInterfaces();

    for (const i of Object.keys(this.interfaces)) {
      const iface = this.interfaces[i];
      interfaces.push(iface.$introspect());
    }

    return interfaces;
  }

  static defaultInterfaces () {
    return [
      {
        $: { name: 'org.freedesktop.DBus.Introspectable' },
        method: [
          {
            $: { name: 'Introspect' },
            arg: [
              {
                $: { name: 'data', direction: 'out', type: 's' }
              }
            ]
          }
        ]
      },
      {
        $: { name: 'org.freedesktop.DBus.Peer' },
        method: [
          {
            $: { name: 'GetMachineId' },
            arg: [
              { $: { direction: 'out', name: 'machine_uuid', type: 's' } }
            ]
          },
          {
            $: { name: 'Ping' }
          }
        ]
      },
      {
        $: { name: 'org.freedesktop.DBus.Properties' },
        method: [
          {
            $: { name: 'Get' },
            arg: [
              { $: { direction: 'in', type: 's' } },
              { $: { direction: 'in', type: 's' } },
              { $: { direction: 'out', type: 'v' } }
            ]
          },
          {
            $: { name: 'Set' },
            arg: [
              { $: { direction: 'in', type: 's' } },
              { $: { direction: 'in', type: 's' } },
              { $: { direction: 'in', type: 'v' } }
            ]
          },
          {
            $: { name: 'GetAll' },
            arg: [
              { $: { direction: 'in', type: 's' } },
              { $: { direction: 'out', type: 'a{sv}' } }
            ]
          }
        ],
        signal: [
          {
            $: { name: 'PropertiesChanged' },
            arg: [
              { $: { type: 's' } },
              { $: { type: 'a{sv}' } },
              { $: { type: 'as' } }
            ]
          }
        ]
      }
    ];
  }
}

module.exports = ServiceObject;
