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

import { Buffer } from 'buffer';
import { Graph, Vertex, Edge, VertexProperty } from '../../../graph.js';

export default class GraphSerializer {
  constructor(ioc) {
    this.ioc = ioc;
    this.ioc.serializers[ioc.DataType.GRAPH] = this;
  }

  canBeUsedFor(value) {
    return value instanceof Graph;
  }

  serialize(item, fullyQualifiedFormat = true) {
    if (item === undefined || item === null) {
      if (fullyQualifiedFormat) {
        return Buffer.from([this.ioc.DataType.GRAPH, 0x01]);
      }
      // value-only null fallback: zero vertices + zero edges
      const zeroInt = [0x00, 0x00, 0x00, 0x00];
      return Buffer.from([...zeroInt, ...zeroInt]);
    }

    const bufs = [];
    if (fullyQualifiedFormat) {
      bufs.push(Buffer.from([this.ioc.DataType.GRAPH, 0x00]));
    }

    const vertices = item.vertices ? Array.from(item.vertices.values()) : [];
    const edges = item.edges ? Array.from(item.edges.values()) : [];

    // {vertex_count}
    bufs.push(this.ioc.intSerializer.serialize(vertices.length, false));

    // vertices
    for (const v of vertices) {
      // {id}
      bufs.push(this.ioc.anySerializer.serialize(v.id));

      // {label} as list (value-only)
      const vLabels = v.labels instanceof Set ? Array.from(v.labels) : [v.label];
      bufs.push(this.ioc.listSerializer.serialize(vLabels, false));

      const vps = Array.isArray(v.properties) ? v.properties : [];

      // {vp_count}
      bufs.push(this.ioc.intSerializer.serialize(vps.length, false));

      for (const vp of vps) {
        // {vp_id}
        bufs.push(this.ioc.anySerializer.serialize(vp.id));

        // {vp_label} as 1-element list (value-only)
        bufs.push(this.ioc.listSerializer.serialize([vp.label], false));

        // {vp_value}
        bufs.push(this.ioc.anySerializer.serialize(vp.value));

        // {parent} (always null)
        bufs.push(this.ioc.unspecifiedNullSerializer.serialize(null));

        // {meta_props} as value-only List<Property>
        const metaProps = Array.isArray(vp.properties) ? vp.properties : [];
        bufs.push(this.ioc.listSerializer.serialize(metaProps, false));
      }
    }

    // {edge_count}
    bufs.push(this.ioc.intSerializer.serialize(edges.length, false));

    // edges
    for (const e of edges) {
      // {id}
      bufs.push(this.ioc.anySerializer.serialize(e.id));

      // {label} as list (value-only)
      const eLabels = e.labels instanceof Set ? Array.from(e.labels) : [e.label];
      bufs.push(this.ioc.listSerializer.serialize(eLabels, false));

      // {inV_id}
      bufs.push(this.ioc.anySerializer.serialize(e.inV && e.inV.id));

      // {inV_label} (always null placeholder, fully-qualified)
      bufs.push(this.ioc.unspecifiedNullSerializer.serialize(null));

      // {outV_id}
      bufs.push(this.ioc.anySerializer.serialize(e.outV && e.outV.id));

      // {outV_label} (always null placeholder, fully-qualified)
      bufs.push(this.ioc.unspecifiedNullSerializer.serialize(null));

      // {parent} (always null)
      bufs.push(this.ioc.unspecifiedNullSerializer.serialize(null));

      // {edge_props} as value-only List<Property>
      const props = Array.isArray(e.properties) ? e.properties : [];
      bufs.push(this.ioc.listSerializer.serialize(props, false));
    }

    return Buffer.concat(bufs);
  }

  /**
   * Async deserialization of graph value bytes from a StreamReader.
   * @param {StreamReader} reader
   * @param {number} valueFlag
   * @param {number} typeCode
   * @returns {Promise<Graph>}
   */
  async deserializeValue(reader, valueFlag, typeCode) {
    const graph = new Graph();

    // {vertex_count} bare int
    const vertexCount = await this.ioc.intSerializer.deserializeBare(reader);
    for (let i = 0; i < vertexCount; i++) {
      // {id} fully qualified
      const vId = await this.ioc.anySerializer.deserialize(reader);

      // {label} value-only list - full multi-label list
      const vLabelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST);
      const vLabels = Array.isArray(vLabelList) ? vLabelList : vLabelList ? [vLabelList] : [];
      const vLabel = vLabels.length > 0 ? vLabels[0] : 'vertex';

      const vertex = new Vertex(vId, vLabel, [], vLabels);
      graph.vertices.set(vId, vertex);

      // {vp_count} bare int
      const vpCount = await this.ioc.intSerializer.deserializeBare(reader);
      for (let j = 0; j < vpCount; j++) {
        // {vp_id} fully qualified
        const vpId = await this.ioc.anySerializer.deserialize(reader);

        // {vp_label} value-only list, first element
        const vpLabelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST);
        const vpLabel = Array.isArray(vpLabelList) && vpLabelList.length > 0 ? vpLabelList[0] : vpLabelList;

        // {vp_value} fully qualified
        const vpValue = await this.ioc.anySerializer.deserialize(reader);

        // {parent} fully qualified (always null)
        await this.ioc.anySerializer.deserialize(reader);

        // {meta_props} value-only list
        const metaProps = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST);

        const vp = new VertexProperty(vpId, vpLabel, vpValue, metaProps || []);
        vertex.properties.push(vp);
      }
    }

    // {edge_count} bare int
    const edgeCount = await this.ioc.intSerializer.deserializeBare(reader);
    for (let i = 0; i < edgeCount; i++) {
      // {id} fully qualified
      const eId = await this.ioc.anySerializer.deserialize(reader);

      // {label} value-only list - full multi-label list
      const eLabelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST);
      const eLabels = Array.isArray(eLabelList) ? eLabelList : eLabelList ? [eLabelList] : [];
      const eLabel = eLabels.length > 0 ? eLabels[0] : 'edge';

      // {inV_id} fully qualified
      const inVId = await this.ioc.anySerializer.deserialize(reader);

      // {inV_label} fully qualified (always null placeholder) — discard
      await this.ioc.anySerializer.deserialize(reader);

      // {outV_id} fully qualified
      const outVId = await this.ioc.anySerializer.deserialize(reader);

      // {outV_label} fully qualified (always null placeholder) — discard
      await this.ioc.anySerializer.deserialize(reader);

      // {parent} fully qualified (always null) — discard
      await this.ioc.anySerializer.deserialize(reader);

      // {edge_props} value-only list
      const edgeProps = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST);

      // Reuse vertex instances already in graph.vertices, otherwise build stand-ins
      const inV = graph.vertices.get(inVId) || new Vertex(inVId, '', []);
      const outV = graph.vertices.get(outVId) || new Vertex(outVId, '', []);

      const edge = new Edge(eId, outV, eLabel, inV, edgeProps || [], eLabels);
      graph.edges.set(eId, edge);
    }

    return graph;
  }

  /**
   * Async fully-qualified deserialization from a StreamReader.
   * @param {StreamReader} reader
   * @returns {Promise<Graph|null>}
   */
  async deserialize(reader) {
    const type_code = await reader.readUInt8();
    if (type_code !== this.ioc.DataType.GRAPH) {
      throw new Error(`GraphSerializer: unexpected {type_code}=0x${type_code.toString(16)}`);
    }
    const value_flag = await reader.readUInt8();
    if (value_flag === 0x01) {
      return null;
    }
    if (value_flag !== 0x00) {
      throw new Error(`GraphSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`);
    }
    return this.deserializeValue(reader, value_flag, type_code);
  }
}
