/*
 *  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.
 */

/**
 * @module datastax/graph/tinkerpop/typeSerializers
 * @ignore
 */

/**
 * @author Jorge Bay Gondra
 */
'use strict';

// Replace dependencies to minimize code changes from Apache TinkerPop
const t = {
  P: UnsupportedType, TextP: UnsupportedType, Traversal: UnsupportedType, Traverser: UnsupportedType,
  EnumValue: UnsupportedType
};
const ts = { TraversalStrategy: UnsupportedType };
const Bytecode = UnsupportedType;
const g = require('./index');
const utils = { Long: UnsupportedType };
t.t = g.t;
t.direction = g.direction;

function UnsupportedType() { }

const valueKey = '@value';
const typeKey = '@type';

/**
 * @abstract
 */
class TypeSerializer {
  serialize() {
    throw new Error('serialize() method not implemented for ' + this.constructor.name);
  }

  deserialize() {
    throw new Error('deserialize() method not implemented for ' + this.constructor.name);
  }

  canBeUsedFor() {
    throw new Error('canBeUsedFor() method not implemented for ' + this.constructor.name);
  }
}

class NumberSerializer extends TypeSerializer {
  serialize(item) {
    if (isNaN(item)) {
      return {
        [typeKey]: 'g:Double',
        [valueKey]: 'NaN'
      };
    } else if (item === Number.POSITIVE_INFINITY) {
      return {
        [typeKey]: 'g:Double',
        [valueKey]: 'Infinity'
      };
    } else if (item === Number.NEGATIVE_INFINITY) {
      return {
        [typeKey]: 'g:Double',
        [valueKey]: '-Infinity'
      };
    } else {
      return item;
    }
  }

  deserialize(obj) {
    var val = obj[valueKey];
    if (val === 'NaN') {
      return NaN;
    } else if (val === 'Infinity') {
      return Number.POSITIVE_INFINITY;
    } else if (val === '-Infinity') {
      return Number.NEGATIVE_INFINITY;
    } else {
      return parseFloat(val);
    }
  }

  canBeUsedFor(value) {
    return (typeof value === 'number');
  }
}

class DateSerializer extends TypeSerializer {
  serialize(item) {
    return {
      [typeKey]: 'g:Date',
      [valueKey]: item.getTime()
    };
  }

  deserialize(obj) {
    return new Date(obj[valueKey]);
  }

  canBeUsedFor(value) {
    return (value instanceof Date);
  }
}

class LongSerializer extends TypeSerializer {
  serialize(item) {
    return {
      [typeKey]: 'g:Int64',
      [valueKey]: item.value
    };
  }

  canBeUsedFor(value) {
    return (value instanceof utils.Long);
  }
}

class BytecodeSerializer extends TypeSerializer {
  serialize(item) {
    let bytecode = item;
    if (item instanceof t.Traversal) {
      bytecode = item.getBytecode();
    }
    const result = {};
    result[typeKey] = 'g:Bytecode';
    const resultValue = result[valueKey] = {};
    const sources = this._serializeInstructions(bytecode.sourceInstructions);
    if (sources) {
      resultValue['source'] = sources;
    }
    const steps = this._serializeInstructions(bytecode.stepInstructions);
    if (steps) {
      resultValue['step'] = steps;
    }
    return result;
  }

  _serializeInstructions(instructions) {
    if (instructions.length === 0) {
      return null;
    }
    const result = new Array(instructions.length);
    result[0] = instructions[0];
    for (let i = 0; i < instructions.length; i++) {
      result[i] = instructions[i].map(item => this.writer.adaptObject(item));
    }
    return result;
  }

  canBeUsedFor(value) {
    return (value instanceof Bytecode) || (value instanceof t.Traversal);
  }
}

class PSerializer extends TypeSerializer {
  /** @param {P} item */
  serialize(item) {
    const result = {};
    result[typeKey] = 'g:P';
    const resultValue = result[valueKey] = {
      'predicate': item.operator
    };
    if (item.other === undefined || item.other === null) {
      resultValue['value'] = this.writer.adaptObject(item.value);
    }
    else {
      resultValue['value'] = [ this.writer.adaptObject(item.value), this.writer.adaptObject(item.other) ];
    }
    return result;
  }

  canBeUsedFor(value) {
    return (value instanceof t.P);
  }
}

class TextPSerializer extends TypeSerializer {
  /** @param {TextP} item */
  serialize(item) {
    const result = {};
    result[typeKey] = 'g:TextP';
    const resultValue = result[valueKey] = {
      'predicate': item.operator
    };
    if (item.other === undefined || item.other === null) {
      resultValue['value'] = this.writer.adaptObject(item.value);
    }
    else {
      resultValue['value'] = [ this.writer.adaptObject(item.value), this.writer.adaptObject(item.other) ];
    }
    return result;
  }

  canBeUsedFor(value) {
    return (value instanceof t.TextP);
  }
}

class LambdaSerializer extends TypeSerializer {
  /** @param {Function} item */
  serialize(item) {
    return {
      [typeKey]: 'g:Lambda',
      [valueKey]: {
        'arguments': item.length,
        'language': 'gremlin-javascript',
        'script': item.toString()
      }
    };
  }

  canBeUsedFor(value) {
    return (typeof value === 'function');
  }
}

class EnumSerializer extends TypeSerializer {
  /** @param {EnumValue} item */
  serialize(item) {
    return {
      [typeKey]: 'g:' + item.typeName,
      [valueKey]: item.elementName
    };
  }

  canBeUsedFor(value) {
    return value && value.typeName && value instanceof t.EnumValue;
  }
}

class TraverserSerializer extends TypeSerializer {
  /** @param {Traverser} item */
  serialize(item) {
    return {
      [typeKey]: 'g:Traverser',
      [valueKey]: {
        'value': this.writer.adaptObject(item.object),
        'bulk': this.writer.adaptObject(item.bulk)
      }
    };
  }

  deserialize(obj) {
    const value = obj[valueKey];
    return new t.Traverser(this.reader.read(value['value']), this.reader.read(value['bulk']));
  }

  canBeUsedFor(value) {
    return (value instanceof t.Traverser);
  }
}

class TraversalStrategySerializer extends TypeSerializer {
  /** @param {TraversalStrategy} item */
  serialize(item) {
    return {
      [typeKey]: 'g:' + item.constructor.name,
      [valueKey]: item.configuration
    };
  }

  canBeUsedFor(value) {
    return (value instanceof ts.TraversalStrategy);
  }
}

class VertexSerializer extends TypeSerializer {
  deserialize(obj) {
    const value = obj[valueKey];
    return new g.Vertex(this.reader.read(value['id']), value['label'], this.reader.read(value['properties']));
  }

  /** @param {Vertex} item */
  serialize(item) {
    return {
      [typeKey]: 'g:Vertex',
      [valueKey]: {
        'id': this.writer.adaptObject(item.id),
        'label': item.label
      }
    };
  }

  canBeUsedFor(value) {
    return (value instanceof g.Vertex);
  }
}

class VertexPropertySerializer extends TypeSerializer {
  deserialize(obj) {
    const value = obj[valueKey];
    return new g.VertexProperty(
      this.reader.read(value['id']),
      value['label'],
      this.reader.read(value['value']),
      this.reader.read(value['properties'])
    );
  }
}

class PropertySerializer extends TypeSerializer {
  deserialize(obj) {
    const value = obj[valueKey];
    return new g.Property(
      value['key'],
      this.reader.read(value['value']));
  }
}

class EdgeSerializer extends TypeSerializer {
  deserialize(obj) {
    const value = obj[valueKey];
    return new g.Edge(
      this.reader.read(value['id']),
      new g.Vertex(this.reader.read(value['outV']), this.reader.read(value['outVLabel'])),
      value['label'],
      new g.Vertex(this.reader.read(value['inV']), this.reader.read(value['inVLabel'])),
      this.reader.read(value['properties'])
    );
  }

  /** @param {Edge} item */
  serialize(item) {
    return {
      [typeKey]: 'g:Edge',
      [valueKey]: {
        'id': this.writer.adaptObject(item.id),
        'label': item.label,
        'outV': this.writer.adaptObject(item.outV.id),
        'outVLabel': item.outV.label,
        'inV': this.writer.adaptObject(item.inV.id),
        'inVLabel': item.inV.label
      }
    };
  }

  canBeUsedFor(value) {
    return (value instanceof g.Edge);
  }
}

class PathSerializer extends TypeSerializer {
  deserialize(obj) {
    const value = obj[valueKey];
    const objects = value['objects'].map(o => this.reader.read(o));
    return new g.Path(this.reader.read(value['labels']), objects);
  }
}

class Path3Serializer extends TypeSerializer {
  deserialize(obj) {
    const value = obj[valueKey];
    return new g.Path(this.reader.read(value['labels']), this.reader.read(value['objects']));
  }
}

class TSerializer extends TypeSerializer {
  deserialize(obj) {
    return t.t[obj[valueKey]];
  }
}

class DirectionSerializer extends TypeSerializer {
    deserialize(obj) {
        return t.direction[obj[valueKey].toLowerCase()];
    }
}

class ArraySerializer extends TypeSerializer {
  constructor(typeKey) {
    super();
    this.typeKey = typeKey;
  }

  deserialize(obj) {
    const value = obj[valueKey];
    if (!Array.isArray(value)) {
      throw new Error('Expected Array, obtained: ' + value);
    }
    return value.map(x => this.reader.read(x));
  }

  /** @param {Array} item */
  serialize(item) {
    return {
      [typeKey]: this.typeKey,
      [valueKey]: item.map(x => this.writer.adaptObject(x))
    };
  }

  canBeUsedFor(value) {
    return Array.isArray(value);
  }
}

class BulkSetSerializer extends TypeSerializer {
  deserialize(obj) {
      const value = obj[valueKey];
      if (!Array.isArray(value)) {
          throw new Error('Expected Array, obtained: ' + value);
      }

      // coerce the BulkSet to List. if the bulk exceeds the int space then we can't coerce to List anyway,
      // so this query will be trouble. we'd need a legit BulkSet implementation here in js. this current
      // implementation is here to replicate the previous functionality that existed on the server side in
      // previous versions.
      let result = [];
      for (let ix = 0, iy = value.length; ix < iy; ix += 2) {
        const pair = value.slice(ix, ix + 2);
        result = result.concat(Array(this.reader.read(pair[1])).fill(this.reader.read(pair[0])));
      }

      return result;
  }
}

class MapSerializer extends TypeSerializer {
  deserialize(obj) {
    const value = obj[valueKey];
    if (!Array.isArray(value)) {
      throw new Error('Expected Array, obtained: ' + value);
    }
    const result = new Map();
    for (let i = 0; i < value.length; i += 2) {
      result.set(this.reader.read(value[i]), this.reader.read(value[i + 1]));
    }
    return result;
  }

  /** @param {Map} map */
  serialize(map) {
    const arr = [];
    map.forEach((v, k) => {
      arr.push(this.writer.adaptObject(k));
      arr.push(this.writer.adaptObject(v));
    });
    return {
      [typeKey]: 'g:Map',
      [valueKey]: arr
    };
  }

  canBeUsedFor(value) {
    return value instanceof Map;
  }
}

class ListSerializer extends ArraySerializer {
  constructor() {
    super('g:List');
  }
}

class SetSerializer extends ArraySerializer {
  constructor() {
    super('g:Set');
  }
}

module.exports = {
  BulkSetSerializer,
  BytecodeSerializer,
  DateSerializer,
  DirectionSerializer,
  EdgeSerializer,
  EnumSerializer,
  LambdaSerializer,
  ListSerializer,
  LongSerializer,
  MapSerializer,
  NumberSerializer,
  Path3Serializer,
  PathSerializer,
  PropertySerializer,
  PSerializer,
  TextPSerializer,
  SetSerializer,
  TSerializer,
  TraverserSerializer,
  TraversalStrategySerializer,
  typeKey,
  valueKey,
  VertexPropertySerializer,
  VertexSerializer
};
