const { mergeQueryProps } = require('./modelQueryProps');
const { isObject, cloneDeep } = require('../utils/objectUtils');

function toJson(model, opt) {
  const modelClass = model.constructor;

  if (!isObject(opt)) {
    opt = {};
  }

  // We don't take a copy of `opt` here which can cause some problems in some
  // super rare cases since we modify an object passed from the outside. I can't
  // think of a realistic scenario where this would actually have some unwanted
  // effects. We don't take a copy for performance reasons.
  opt.omit = null;
  opt.pick = null;
  opt.omitFromJson = model.$omitFromJson() || null;

  if (opt.virtuals === undefined) {
    opt.virtuals = true;
  }

  if (opt.shallow) {
    opt.omit = modelClass.getRelations();
  }

  let json = toExternalJsonImpl(model, opt);
  return model.$formatJson(json);
}

function toDatabaseJson(model, knex) {
  const modelClass = model.constructor;
  const jsonSchema = modelClass.getJsonSchema();

  const opt = {
    virtuals: false,
    shallow: true,
    omit: modelClass.getRelations(),
    pick: (jsonSchema && modelClass.pickJsonSchemaProperties && jsonSchema.properties) || null,
    omitFromJson: model.$omitFromDatabaseJson() || null
  };

  let json = toDatabaseJsonImpl(model, opt);
  json = model.$formatDatabaseJson(json);

  return mergeQueryProps(model, json, knex);
}

function toExternalJsonImpl(model, opt) {
  const json = {};
  const keys = Object.keys(model);
  const vAttr = model.constructor.virtualAttributes;

  for (let i = 0, l = keys.length; i < l; ++i) {
    const key = keys[i];
    const value = model[key];

    assignJsonValue(json, key, value, opt);
  }

  if (vAttr && opt.virtuals === true) {
    assignVirtualAttributes(json, model, vAttr, opt);
  }

  return json;
}

function toDatabaseJsonImpl(model, opt) {
  const json = {};
  const keys = Object.keys(model);

  for (let i = 0, l = keys.length; i < l; ++i) {
    const key = keys[i];
    const value = model[key];

    assignJsonValue(json, key, value, opt);
  }

  return json;
}

function assignJsonValue(json, key, value, opt) {
  const type = typeof value;

  if (
    (opt.omit === null || !(key in opt.omit)) &&
    (opt.pick === null || key in opt.pick) &&
    (opt.omitFromJson === null || opt.omitFromJson.indexOf(key) === -1) &&
    type !== 'function' &&
    type !== 'undefined' &&
    key[0] !== '$'
  ) {
    if (isObject(value)) {
      json[key] = toJsonObject(value, opt);
    } else {
      json[key] = value;
    }
  }
}

function assignVirtualAttributes(json, model, vAttr, opt) {
  for (let i = 0, l = vAttr.length; i < l; ++i) {
    const key = vAttr[i];
    let value = model[key];

    if (typeof value === 'function') {
      value = value.call(model);
    }

    assignJsonValue(json, key, value, opt);
  }
}

function toJsonObject(value, opt) {
  if (Array.isArray(value)) {
    return toJsonArray(value, opt);
  } else if (value.$isObjectionModel) {
    // No branch for $toDatabaseJson here since there is never a need
    // to have nested models in database rows.
    return value.$toJson(opt);
  } else if (Buffer.isBuffer(value)) {
    return value;
  } else {
    return cloneDeep(value);
  }
}

function toJsonArray(value, opt) {
  const ret = new Array(value.length);

  for (let i = 0, l = ret.length; i < l; ++i) {
    const item = value[i];

    if (isObject(item)) {
      ret[i] = toJsonObject(item, opt);
    } else {
      ret[i] = item;
    }
  }

  return ret;
}

module.exports = {
  toJson,
  toDatabaseJson
};
