// Shared libraries
import * as Util from '@dra2020/util';

// Format for efficient packing of multipolygon coordinates
//
// [n] number of polygons
// [n polygon headers]...
//  [n] number of rings
//    [n]... number of coordinates in ring
//  followed by [x,y]... for all the coordinates

export interface PolyPack
{
  offset: number;
  length: number;
  buffer: any;
}

export function polyPackSize(coords: any): number
{
  if (Util.depthof(coords) == 4) coords = [ coords ]; // normalize to multipolygon
  let nFloats: number = 2;  // for number of polygons and size of header block
  let i: number, j: number;
  let p: any, r: any;
  for (i = 0; i < coords.length; i++)
  {
    p = coords[i];
    nFloats++;  // For number of rings in this polygon
    for (j = 0; j < p.length; j++)
    {
      nFloats++;  // For number of coordinates in this ring
      r = p[j];
      nFloats += r.length * 2;  // for each x, y pair
    }
  }
  return nFloats;
}

type EachRingCB = (buffer: Float64Array, iPoly: number, iRing: number, iOffset: number, nPoints: number) => void;
type EachPointCB = (buffer: Float64Array, iPoly: number, iRing: number, iOffset: number) => void;

export function polyPackEachRing(pack: PolyPack, cb: EachRingCB): void
{
  if (pack == null || pack.buffer == null) return;

  if (pack.offset === undefined) throw 'oops: not a PolyPack';

  let b = pack.buffer;
  let iRing: number;
  let iOffset: number = pack.offset;
  let cOffset: number = b[iOffset+1];

  // Grab number of polygons
  let nPoly: number = b[iOffset++];
  // Move past header length
  iOffset++;
  for (let iPoly: number = 0; iPoly < nPoly; iPoly++)
  {
    // Grab number of rings in this polygon
    let nRing: number = b[iOffset++];
    for (let iRing: number = 0; iRing < nRing; iRing++)
    {
      // Grab number of points in this ring
      let nPoints: number = b[iOffset++];
      cb(b, iPoly, iRing, cOffset, nPoints);
      cOffset += nPoints * 2;
    }
  }
}

export function polyPackEachPoint(pack: PolyPack, cb: EachPointCB): void
{
  polyPackEachRing(pack, (b: Float64Array, iPoly: number, iRing: number, iOffset: number, nPoints: number) => {
      let iEnd: number = iOffset + (nPoints * 2);
      for (; iOffset < iEnd; iOffset += 2)
        cb(b, iPoly, iRing, iOffset);
    });
}

export function polyPack(coords: any, prepack?: PolyPack): PolyPack
{
  // Null feature?
  if (coords == null || (coords.length !== undefined && coords.length == 0)) return null;

  // Actually already a PolyPack?
  if (coords.offset !== undefined) return coords as PolyPack;

  // Packed feature?
  if (coords && coords.geometry && coords.geometry.packed)
    return coords.geometry.packed as PolyPack;

  // Unpacked feature, grab the coordinates array
  if (coords && coords.geometry)
    coords = coords.geometry.coordinates;

  // Transparently handle polygon or multi-polygon
  if (Util.depthof(coords) == 4) coords = [ coords ];

  let nFloats = polyPackSize(coords);
  let i: number, j: number, k: number;
  let p: any, r: any;

  // Allocate typed buffer if not passed in
  if (prepack === undefined)
  {
    let ab = new ArrayBuffer(nFloats * 8);
    prepack = { offset: 0, length: nFloats, buffer: new Float64Array(ab) };
  }
  else
  {
    prepack = Util.shallowCopy(prepack);
    prepack.length = nFloats;
  }

  let af: Float64Array = prepack.buffer as Float64Array;

  // Fill in header
  let z: number = prepack.offset;
  af[z++] = coords.length;
  z++;  // spot for header size
  for (i = 0; i < coords.length; i++)
  {
    p = coords[i];
    af[z++] = p.length;
    for (j = 0; j < p.length; j++)
      af[z++] = p[j].length;
  }

  // set header size back in header
  af[prepack.offset+1] = z;

  // Fill in coords
  for (i = 0; i < coords.length; i++)
  {
    p = coords[i];
    for (j = 0; j < p.length; j++)
    {
      r = p[j];
      for (k = 0; k < r.length; k++)
      {
        af[z++] = r[k][0];
        af[z++] = r[k][1];
      }
    }
  }

  return prepack;
}

export function polyUnpack(prepack: any): any
{
  // Check if not packed
  if (prepack.offset === undefined) return prepack;

  let i: number, j: number, k: number;
  let nPolys: number, nRings: number, nCoords: number;
  let p: any, r: any;
  let af: Float64Array = prepack.buffer as Float64Array;
  let coords: any = [];
  let h: number = prepack.offset as number;
  nPolys = af[h++];
  let z = af[h++];  // start of coordinates
  for (i = 0; i < nPolys; i++)
  {
    p = [];
    coords[i] = p;
    nRings = af[h++];
    for (j = 0; j < nRings; j++)
    {
      r = [];
      p[j] = r;
      nCoords = af[h++];
      for (k = 0; k < nCoords; k++)
        r[k] = [ af[z++], af[z++] ];
    }
  }

  return coords.length > 1 ? coords : coords[0];
}

export function featurePackSize(f: any): number
{
  if (f && f.geometry && f.geometry.coordinates)
    return polyPackSize(f.geometry.coordinates);
  return 0;
}

export function featurePack(f: any, prepack?: PolyPack): any
{
  if (f && f.geometry && f.geometry.coordinates)
  {
    f.geometry.packed = polyPack(f.geometry.coordinates, prepack);
    delete f.geometry.coordinates;
    return f.geometry.packed;
  }
  else if (f.type && f.type === 'FeatureCollection' && f.features)
  {
    // Allocate one large buffer
    let nFloats: number = 0;
    let i: number;
    for (i = 0; i < f.features.length; i++)
      nFloats += featurePackSize(f.features[i]);
    let ab = new ArrayBuffer(nFloats * 8);
    prepack = { offset: 0, length: nFloats, buffer: new Float64Array(ab) };

    // Now pack
    for (i = 0; i < f.features.length; i++)
    {
      let postpack = featurePack(f.features[i], prepack);
      prepack.offset += postpack.length;
    }
    if (prepack.offset != nFloats)
      throw 'oops, packing bug';
    return prepack;
  }
  return prepack;
}

export function featureUnpack(f: any): any
{
  if (f && f.geometry && f.geometry.packed)
  {
    f.geometry.coordinates = polyUnpack(f.geometry.packed);
    delete f.geometry.packed;
  }
  else if (f.type && f.type === 'FeatureCollection' && f.features)
  {
    for (let i: number = 0; i < f.features.length; i++)
      featureUnpack(f.features[i]);
  }
  return f;
}
