/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */

/**
 * @author Jorge Bay Gondra
 */

'use strict';

const itemDone = Object.freeze({ value: null, done: true });
const asyncIteratorSymbol = Symbol.asyncIterator || Symbol('@@asyncIterator');

class Traversal {
  constructor(graph, traversalStrategies, bytecode) {
    this.graph = graph;
    this.traversalStrategies = traversalStrategies;
    this.bytecode = bytecode;
    /** @type {Array<Traverser>} */
    this.traversers = null;
    this.sideEffects = null;
    this._traversalStrategiesPromise = null;
    this._traversersIteratorIndex = 0;
  }

  /**
   * Async iterable method implementation.
   */
  [asyncIteratorSymbol]() {
    return this;
  }

  /** @returns {Bytecode} */
  getBytecode() {
    return this.bytecode;
  }

  /**
   * Returns an Array containing the traverser objects.
   * @returns {Promise.<Array>}
   */
  toList() {
    return this._applyStrategies().then(() => {
      const result = [];
      let it;
      while ((it = this._getNext()) && !it.done) {
        result.push(it.value);
      }
      return result;
    });
  }

  /**
   * Determines if there are any more items to iterate from the traversal.
   * @returns {Promise.<boolean>}
   */
  hasNext() {
    return this._applyStrategies().then(
      () =>
        this.traversers &&
        this.traversers.length > 0 &&
        this._traversersIteratorIndex < this.traversers.length &&
        this.traversers[this._traversersIteratorIndex].bulk > 0,
    );
  }

  /**
   * Iterates all Traverser instances in the traversal.
   * @returns {Promise}
   */
  iterate() {
    this.bytecode.addStep('none');
    return this._applyStrategies().then(() => {
      let it;
      while ((it = this._getNext()) && !it.done) {
        //
      }
    });
  }

  /**
   * Async iterator method implementation.
   * Returns a promise containing an iterator item.
   * @returns {Promise.<{value, done}>}
   */
  next() {
    return this._applyStrategies().then(() => this._getNext());
  }

  /**
   * Synchronous iterator of traversers including
   * @private
   */
  _getNext() {
    while (this.traversers && this._traversersIteratorIndex < this.traversers.length) {
      const traverser = this.traversers[this._traversersIteratorIndex];
      if (traverser.bulk > 0) {
        traverser.bulk--;
        return { value: traverser.object, done: false };
      }
      this._traversersIteratorIndex++;
    }
    return itemDone;
  }

  _applyStrategies() {
    if (this._traversalStrategiesPromise) {
      // Apply strategies only once
      return this._traversalStrategiesPromise;
    }
    return (this._traversalStrategiesPromise = this.traversalStrategies.applyStrategies(this));
  }

  /**
   * Returns step instructions during JSON serialization
   * @returns {Array}
   */
  toJSON() {
    return this.bytecode.stepInstructions;
  }

  /**
   * Returns the Bytecode JSON representation of the traversal
   * @returns {String}
   */
  toString() {
    return this.bytecode.toString();
  }
}

class IO {
  static get graphml() {
    return 'graphml';
  }

  static get graphson() {
    return 'graphson';
  }

  static get gryo() {
    return 'gryo';
  }

  static get reader() {
    return '~tinkerpop.io.reader';
  }

  static get registry() {
    return '~tinkerpop.io.registry';
  }

  static get writer() {
    return '~tinkerpop.io.writer';
  }
}

// eslint-disable-next-line no-unused-vars
class ConnectedComponent {
  static get component() {
    return 'gremlin.connectedComponentVertexProgram.component';
  }

  static get edges() {
    return '~tinkerpop.connectedComponent.edges';
  }

  static get propertyName() {
    return '~tinkerpop.connectedComponent.propertyName';
  }
}

// eslint-disable-next-line no-unused-vars
class ShortestPath {
  static get distance() {
    return '~tinkerpop.shortestPath.distance';
  }

  static get edges() {
    return '~tinkerpop.shortestPath.edges';
  }

  static get includeEdges() {
    return '~tinkerpop.shortestPath.includeEdges';
  }

  static get maxDistance() {
    return '~tinkerpop.shortestPath.maxDistance';
  }

  static get target() {
    return '~tinkerpop.shortestPath.target';
  }
}

// eslint-disable-next-line no-unused-vars
class PageRank {
  static get edges() {
    return '~tinkerpop.pageRank.edges';
  }

  static get propertyName() {
    return '~tinkerpop.pageRank.propertyName';
  }

  static get times() {
    return '~tinkerpop.pageRank.times';
  }
}

// eslint-disable-next-line no-unused-vars
class PeerPressure {
  static get edges() {
    return '~tinkerpop.peerPressure.edges';
  }

  static get propertyName() {
    return '~tinkerpop.peerPressure.propertyName';
  }

  static get times() {
    return '~tinkerpop.peerPressure.times';
  }
}

class P {
  /**
   * Represents an operation.
   * @constructor
   */
  constructor(operator, value, other) {
    this.operator = operator;
    this.value = value;
    this.other = other;
  }

  /**
   * Returns the string representation of the instance.
   * @returns {string}
   */
  toString() {
    function formatValue(value) {
      if (Array.isArray(value)) {
        const acc = [];
        for (const item of value) {
          acc.push(formatValue(item));
        }
        return acc;
      }
      if (value && typeof value === 'string') {
        return `'${value}'`;
      }
      return value;
    }

    if (this.other === undefined || this.other === null) {
      return this.operator + '(' + formatValue(this.value) + ')';
    }
    return this.operator + '(' + formatValue(this.value) + ', ' + formatValue(this.other) + ')';
  }

  and(arg) {
    return new P('and', this, arg);
  }

  or(arg) {
    return new P('or', this, arg);
  }

  static within(...args) {
    if (args.length === 1 && Array.isArray(args[0])) {
      return new P('within', args[0], null);
    }
    return new P('within', args, null);
  }

  static without(...args) {
    if (args.length === 1 && Array.isArray(args[0])) {
      return new P('without', args[0], null);
    }
    return new P('without', args, null);
  }

  /** @param {...Object} args */
  static between(...args) {
    return createP('between', args);
  }

  /** @param {...Object} args */
  static eq(...args) {
    return createP('eq', args);
  }

  /** @param {...Object} args */
  static gt(...args) {
    return createP('gt', args);
  }

  /** @param {...Object} args */
  static gte(...args) {
    return createP('gte', args);
  }

  /** @param {...Object} args */
  static inside(...args) {
    return createP('inside', args);
  }

  /** @param {...Object} args */
  static lt(...args) {
    return createP('lt', args);
  }

  /** @param {...Object} args */
  static lte(...args) {
    return createP('lte', args);
  }

  /** @param {...Object} args */
  static neq(...args) {
    return createP('neq', args);
  }

  /** @param {...Object} args */
  static not(...args) {
    return createP('not', args);
  }

  /** @param {...Object} args */
  static outside(...args) {
    return createP('outside', args);
  }

  /** @param {...Object} args */
  static test(...args) {
    return createP('test', args);
  }
}

function createP(operator, args) {
  args.unshift(null, operator);
  return new (Function.prototype.bind.apply(P, args))();
}

class TextP {
  /**
   * Represents an operation.
   * @constructor
   */
  constructor(operator, value, other) {
    this.operator = operator;
    this.value = value;
    this.other = other;
  }

  /**
   * Returns the string representation of the instance.
   * @returns {string}
   */
  toString() {
    function formatValue(value) {
      if (value && typeof value === 'string') {
        return `'${value}'`;
      }
      return value;
    }

    if (this.other === undefined) {
      return this.operator + '(' + formatValue(this.value) + ')';
    }
    return this.operator + '(' + formatValue(this.value) + ', ' + formatValue(this.other) + ')';
  }

  and(arg) {
    return new P('and', this, arg);
  }

  or(arg) {
    return new P('or', this, arg);
  }

  /** @param {...Object} args */
  static containing(...args) {
    return createTextP('containing', args);
  }

  /** @param {...Object} args */
  static endingWith(...args) {
    return createTextP('endingWith', args);
  }

  /** @param {...Object} args */
  static notContaining(...args) {
    return createTextP('notContaining', args);
  }

  /** @param {...Object} args */
  static notEndingWith(...args) {
    return createTextP('notEndingWith', args);
  }

  /** @param {...Object} args */
  static notStartingWith(...args) {
    return createTextP('notStartingWith', args);
  }

  /** @param {...Object} args */
  static startingWith(...args) {
    return createTextP('startingWith', args);
  }

  /** @param {...Object} args */
  static regex(...args) {
    return createTextP('regex', args);
  }

  /** @param {...Object} args */
  static notRegex(...args) {
    return createTextP('notRegex', args);
  }
}

function createTextP(operator, args) {
  args.unshift(null, operator);
  return new (Function.prototype.bind.apply(TextP, args))();
}

class Traverser {
  constructor(object, bulk) {
    this.object = object;
    this.bulk = bulk || 1;
  }
}

class TraversalSideEffects {}

const withOptions = {
  tokens: '~tinkerpop.valueMap.tokens',
  none: 0,
  ids: 1,
  labels: 2,
  keys: 4,
  values: 8,
  all: 15,
  indexer: '~tinkerpop.index.indexer',
  list: 0,
  map: 1,
};

function toEnum(typeName, keys) {
  const result = {};
  keys.split(' ').forEach((k) => {
    let jsKey = k;
    if (jsKey === jsKey.toUpperCase()) {
      jsKey = jsKey.toLowerCase();
    }
    result[jsKey] = new EnumValue(typeName, k);
  });
  return result;
}

const directionAlias = {
  from_: 'out',
  to: 'in',
};

// for direction enums, maps the same EnumValue object to the enum aliases after creating them
function toDirectionEnum(typeName, keys) {
  const result = toEnum(typeName, keys);
  Object.keys(directionAlias).forEach((k) => {
    result[k] = result[directionAlias[k]];
  });
  return result;
}

class EnumValue {
  constructor(typeName, elementName) {
    this.typeName = typeName;
    this.elementName = elementName;
  }

  toString() {
    return this.elementName;
  }
}

module.exports = {
  EnumValue,
  P,
  TextP,
  withOptions,
  IO,
  Traversal,
  TraversalSideEffects,
  Traverser,
  barrier: toEnum('Barrier', 'normSack'),
  cardinality: toEnum('Cardinality', 'list set single'),
  column: toEnum('Column', 'keys values'),
  direction: toDirectionEnum('Direction', 'BOTH IN OUT from_ to'),
  graphSONVersion: toEnum('GraphSONVersion', 'V1_0 V2_0 V3_0'),
  gryoVersion: toEnum('GryoVersion', 'V1_0 V3_0'),
  merge: toEnum('Merge', 'onCreate onMatch outV inV'),
  operator: toEnum('Operator', 'addAll and assign div max min minus mult or sum sumLong'),
  order: toEnum('Order', 'asc desc shuffle'),
  pick: toEnum('Pick', 'any none'),
  pop: toEnum('Pop', 'all first last mixed'),
  scope: toEnum('Scope', 'global local'),
  t: toEnum('T', 'id key label value'),
};
