// ----------------------------------------------------------------------------
// capitalize provided string
// ----------------------------------------------------------------------------

export function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

// ----------------------------------------------------------------------------
// Add isA function and register your class name
// ----------------------------------------------------------------------------

function isA(publicAPI, model = {}, name = null) {
  if (!model.isA) {
    model.isA = [];
  }

  if (name) {
    model.isA.push(name);
  }

  if (!publicAPI.isA) {
    publicAPI.isA = (className) => model.isA.indexOf(className) !== -1;
  }
}

// ----------------------------------------------------------------------------
// Basic setter
// ----------------------------------------------------------------------------

function set(publicAPI, model = {}, names = []) {
  names.forEach((name) => {
    publicAPI[`set${capitalize(name)}`] = (value) => {
      model[name] = value;
    };
  });
}

// ----------------------------------------------------------------------------
// Basic getter
// ----------------------------------------------------------------------------

function get(publicAPI, model = {}, names = []) {
  names.forEach((name) => {
    publicAPI[`get${capitalize(name)}`] = () => model[name];
  });
}

// ----------------------------------------------------------------------------
// Add destroy function
// ----------------------------------------------------------------------------

function destroy(publicAPI, model = {}) {
  const previousDestroy = publicAPI.destroy;

  if (!model.subscriptions) {
    model.subscriptions = [];
  }

  publicAPI.destroy = () => {
    if (previousDestroy) {
      previousDestroy();
    }
    while (model.subscriptions && model.subscriptions.length) {
      model.subscriptions.pop().unsubscribe();
    }
    Object.keys(model).forEach((field) => {
      delete model[field];
    });

    // Flag the instance being deleted
    model.deleted = true;
  };
}

// ----------------------------------------------------------------------------
// Event handling: onXXX(callback), fireXXX(args...) or invokeXXX(args...)
// ----------------------------------------------------------------------------

function event(publicAPI, model, eventName, asynchronous = true) {
  const callbacks = [];
  const previousDestroy = publicAPI.destroy;

  function off(index) {
    callbacks[index] = null;
  }

  function on(index) {
    function unsubscribe() {
      off(index);
    }
    return Object.freeze({ unsubscribe });
  }

  const triggerEvent = (...args) => {
    if (model.deleted) {
      console.log("instance deleted - can not call any method");
      return;
    }

    function processCallbacks() {
      callbacks.forEach((callback) => {
        if (callback) {
          try {
            callback.apply(publicAPI, args);
          } catch (errObj) {
            console.log("Error event:", eventName, errObj);
          }
        }
      });
    }

    if (asynchronous) {
      setTimeout(processCallbacks, 0);
    } else {
      processCallbacks();
    }
  };

  publicAPI[`fire${capitalize(eventName)}`] = triggerEvent;
  publicAPI[`invoke${capitalize(eventName)}`] = triggerEvent;

  publicAPI[`on${capitalize(eventName)}`] = (callback) => {
    if (model.deleted) {
      console.log("instance deleted - can not call any method");
      return null;
    }

    const index = callbacks.length;
    callbacks.push(callback);
    return on(index);
  };

  publicAPI.destroy = () => {
    previousDestroy();
    callbacks.forEach((el, index) => off(index));
  };
}

// ----------------------------------------------------------------------------
// Chain function calls
// ----------------------------------------------------------------------------

function chain(...fn) {
  return (...args) => fn.filter((i) => !!i).forEach((i) => i(...args));
}

// ----------------------------------------------------------------------------
// newInstance
// ----------------------------------------------------------------------------

function newInstance(extend) {
  return (initialValues = {}) => {
    const model = {};
    const publicAPI = {};
    extend(publicAPI, model, initialValues);
    return Object.freeze(publicAPI);
  };
}

export default {
  chain,
  destroy,
  event,
  get,
  isA,
  newInstance,
  set,
};
