const parser = require('./parsers/relationExpressionParser');
const { isObject, cloneDeep, values, union } = require('../utils/objectUtils');

class RelationExpression {
  constructor(node = {}, recursionDepth = 0) {
    this.$name = node.$name || null;
    this.$relation = node.$relation || null;
    this.$modify = node.$modify || [];
    this.$recursive = node.$recursive || false;
    this.$allRecursive = node.$allRecursive || false;

    const childNames = getChildNames(node);
    for (let i = 0, l = childNames.length; i < l; ++i) {
      const childName = childNames[i];
      this[childName] = node[childName];
    }

    // These are non-enumerable so that the enumerable interface of this
    // class instance is the same as the result from relationExpressionParser.
    Object.defineProperties(this, {
      recursionDepth: {
        enumerable: false,
        value: recursionDepth
      },

      childNames: {
        enumerable: false,
        value: childNames
      },

      rawNode: {
        enumerable: false,
        value: node
      }
    });
  }

  // Create a relation expression from a string, a pojo or another
  // RelationExpression instance.
  static create(expr) {
    if (isObject(expr)) {
      if (expr.isObjectionRelationExpression) {
        return expr;
      } else {
        return new RelationExpression(normalizeNode(expr));
      }
    } else if (typeof expr === 'string') {
      if (expr.trim().length === 0) {
        return new RelationExpression();
      } else {
        return new RelationExpression(parser.parse(expr));
      }
    } else {
      return new RelationExpression();
    }
  }

  // Create a relation expression from a model graph.
  static fromModelGraph(graph) {
    if (!graph) {
      return new RelationExpression();
    } else {
      return new RelationExpression(modelGraphToNode(graph, newNode()));
    }
  }

  get numChildren() {
    return this.childNames.length;
  }

  get maxRecursionDepth() {
    if (typeof this.$recursive === 'number') {
      return this.$recursive;
    } else {
      return this.$recursive ? Number.MAX_SAFE_INTEGER : 0;
    }
  }

  get isAllRecursive() {
    return this.$allRecursive;
  }

  // Merges this relation expression with another. `expr` can be a string,
  // a pojo, or a RelationExpression instance.
  merge(expr) {
    return new RelationExpression(mergeNodes(this, RelationExpression.create(expr)));
  }

  // Returns true if `expr` is contained by this expression. For example
  // `a.b` is contained by `a.[b, c]`.
  isSubExpression(expr) {
    expr = RelationExpression.create(expr);

    if (this.isAllRecursive) {
      return true;
    }

    if (expr.isAllRecursive) {
      return this.isAllRecursive;
    }

    if (this.$relation !== expr.$relation) {
      return false;
    }

    const maxRecursionDepth = expr.maxRecursionDepth;

    if (maxRecursionDepth > 0) {
      return this.isAllRecursive || this.maxRecursionDepth >= maxRecursionDepth;
    }

    for (let i = 0, l = expr.childNames.length; i < l; ++i) {
      const childName = expr.childNames[i];
      const ownSubExpression = this.childExpression(childName);
      const subExpression = expr.childExpression(childName);

      if (!ownSubExpression || !ownSubExpression.isSubExpression(subExpression)) {
        return false;
      }
    }

    return true;
  }

  // Returns a RelationExpression for a child node or null if there
  // is no child with the given name `childName`.
  childExpression(childName) {
    if (
      this.isAllRecursive ||
      (childName === this.$name && this.recursionDepth < this.maxRecursionDepth - 1)
    ) {
      return new RelationExpression(this, this.recursionDepth + 1);
    }

    const child = this[childName];

    if (child) {
      return new RelationExpression(child, 0);
    } else {
      return null;
    }
  }

  // Loops throught all first level children. `allRelations` must be
  // the return value of `Model.getRelations()` where `Model` is the
  // root model of the expression.
  forEachChildExpression(allRelations, cb) {
    const maxRecursionDepth = this.maxRecursionDepth;

    if (this.isAllRecursive) {
      const relationNames = Object.keys(allRelations);

      for (let i = 0, l = relationNames.length; i < l; ++i) {
        const relationName = relationNames[i];
        const node = newNode(relationName, true);
        const relation = allRelations[relationName];
        const childExpr = new RelationExpression(node, 0);

        cb(childExpr, relation);
      }
    } else if (this.recursionDepth < maxRecursionDepth - 1) {
      const relation = allRelations[this.$name] || null;
      const childExpr = new RelationExpression(this, this.recursionDepth + 1);

      cb(childExpr, relation);
    } else if (maxRecursionDepth === 0) {
      const childNames = this.childNames;

      for (let i = 0, l = childNames.length; i < l; ++i) {
        const childName = childNames[i];
        const node = this[childName];
        const relation = allRelations[node.$relation] || null;
        const childExpr = new RelationExpression(node, 0);

        cb(childExpr, relation);
      }
    }
  }

  expressionsAtPath(path) {
    return findExpressionsAtPath(this, RelationExpression.create(path), []);
  }

  clone() {
    const node = {
      $name: this.$name,
      $relation: this.$relation,
      $modify: this.$modify.slice(),
      $recursive: this.$recursive,
      $allRecursive: this.$allRecursive
    };

    for (let i = 0, l = this.childNames.length; i < l; ++i) {
      const childName = this.childNames[i];
      node[childName] = cloneDeep(this[childName]);
    }

    return new RelationExpression(node, this.recursionDepth);
  }

  toString() {
    return toString(this);
  }

  toJSON() {
    return toJSON(this);
  }
}

// All enumerable properties of a node that don't start with `$`
// are child nodes.
function getChildNames(node) {
  const allKeys = Object.keys(node);
  const childNames = [];

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

    if (key[0] !== '$') {
      childNames.push(key);
    }
  }

  return childNames;
}

function toString(node) {
  const childNames = getChildNames(node);

  let childExpr = childNames.map(childName => node[childName]).map(toString);
  let str = node.$relation;

  if (node.$recursive) {
    if (typeof node.$recursive === 'number') {
      str += '.^' + node.$recursive;
    } else {
      str += '.^';
    }
  } else if (node.$allRecursive) {
    str += '.*';
  }

  if (childExpr.length > 1) {
    childExpr = `[${childExpr.join(', ')}]`;
  } else {
    childExpr = childExpr[0];
  }

  if (node.$modify.length) {
    str += `(${node.$modify.join(', ')})`;
  }

  if (node.$name !== node.$relation) {
    str += ` as ${node.$name}`;
  }

  if (childExpr) {
    if (str) {
      return `${str}.${childExpr}`;
    } else {
      return childExpr;
    }
  } else {
    return str;
  }
}

function toJSON(node, nodeName = null) {
  const json = {};

  if (node.$name && node.$name !== nodeName) {
    json.$name = node.$name;
  }

  if (node.$relation && node.$relation !== nodeName) {
    json.$relation = node.$relation;
  }

  if (!Array.isArray(node.$modify) || node.$modify.length > 0) {
    json.$modify = node.$modify.slice();
  }

  if (node.$recursive) {
    json.$recursive = node.$recursive;
  }

  if (node.$allRecursive) {
    json.$allRecursive = node.$allRecursive;
  }

  const childNames = getChildNames(node);

  for (let i = 0, l = childNames.length; i < l; ++i) {
    const childName = childNames[i];
    const childNode = node[childName];
    const childJson = toJSON(childNode, childName);

    if (Object.keys(childJson).length === 0) {
      json[childName] = true;
    } else {
      json[childName] = childJson;
    }
  }

  return json;
}

function modelGraphToNode(models, node) {
  if (!models) {
    return;
  }

  if (Array.isArray(models)) {
    for (let i = 0, l = models.length; i < l; ++i) {
      modelToNode(models[i], node);
    }
  } else {
    modelToNode(models, node);
  }

  return node;
}

// TODO: recursion check
function modelToNode(model, node) {
  const modelClass = model.constructor;
  const relations = modelClass.getRelationArray();

  for (let r = 0, lr = relations.length; r < lr; ++r) {
    const relName = relations[r].name;

    if (model.hasOwnProperty(relName)) {
      let childNode = node[relName];

      if (!childNode) {
        childNode = newNode(relName);
        node[relName] = childNode;
      }

      modelGraphToNode(model[relName], childNode);
    }
  }
}

function newNode(name, allRecusive = false) {
  return {
    $name: name || null,
    $relation: name || null,
    $modify: [],
    $recursive: false,
    $allRecursive: allRecusive
  };
}

function normalizeNode(node, name = null) {
  const normalized = {
    $name: node.$name || name,
    $relation: node.$relation || name,
    $modify: node.$modify || [],
    $recursive: node.$recursive || false,
    $allRecursive: node.$allRecursive || false
  };

  const childNames = getChildNames(node);

  for (let i = 0, l = childNames.length; i < l; ++i) {
    const childName = childNames[i];
    const childNode = node[childName];

    if (isObject(childNode) || childNode === true) {
      normalized[childName] = normalizeNode(childNode, childName);
    }
  }

  return normalized;
}

function findExpressionsAtPath(target, path, results) {
  if (path.childNames.length == 0) {
    // Path leaf reached, add target node to result set.
    results.push(target);
  } else {
    for (let i = 0, l = path.childNames.length; i < l; ++i) {
      const childName = path.childNames[i];
      const pathChild = path.childExpression(childName);
      const targetChild = target.childExpression(childName);

      if (targetChild) {
        findExpressionsAtPath(targetChild, pathChild, results);
      }
    }
  }

  return results;
}

function mergeNodes(node1, node2) {
  const node = {
    $name: node1.$name,
    $relation: node1.$relation,
    $modify: union(node1.$modify, node2.$modify),
    $recursive: mergeRecursion(node1.$recursive, node2.$recursive),
    $allRecursive: node1.$allRecursive || node2.$allRecursive
  };

  if (!node.$recursive && !node.$allRecursive) {
    const childNames = union(getChildNames(node1), getChildNames(node2));

    for (let i = 0, l = childNames.length; i < l; ++i) {
      const childName = childNames[i];
      const child1 = node1[childName];
      const child2 = node2[childName];

      if (child1 && child2) {
        node[childName] = mergeNodes(child1, child2);
      } else {
        node[childName] = child1 || child2;
      }
    }
  }

  return node;
}

function mergeRecursion(rec1, rec2) {
  if (rec1 === true || rec2 === true) {
    return true;
  } else if (typeof rec1 === 'number' && typeof rec2 === 'number') {
    return Math.max(rec1, rec2);
  } else {
    return rec1 || rec2;
  }
}

Object.defineProperties(RelationExpression.prototype, {
  isObjectionRelationExpression: {
    enumerable: false,
    writable: false,
    value: true
  }
});

module.exports = RelationExpression;
