import {PNG} from 'pngjs';
import {Projection} from '../../utils/types';
import * as d3geo from 'd3-geo';

type ProjectionData = {
  fn: (...args: unknown[]) => d3geo.GeoProjection,
  bounds: (a: [number, number]) => [number, number],
}

export default interface ProjectionService {
  project(equiImage: PNG, projection: Projection): PNG | null
}

export class ProjectionServiceImpl implements ProjectionService {
  private readonly projections: Record<Projection, ProjectionData> = {
    [Projection.EQUIRECTANGULAR]: {
      fn: d3geo.geoEquirectangular,
      bounds: (a) => a,
    },
    [Projection.MERCATOR]: {
      fn: d3geo.geoMercator,
      bounds: ([width, height]) => [width, height * 2],
    },
    [Projection.AZIMUTHAL_EQUIDISTANT]: {
      fn: d3geo.geoAzimuthalEquidistant,
      bounds: (a) => a,
    },
    [Projection.CONIC_CONFORMAL]: {
      fn: d3geo.geoConicConformal,
      bounds: (a) => a,
    },
  } as const

  private readonly referenceWidth = 960 as const;

  project(equiImage: PNG, projection: Projection): PNG | null {
    if (projection === Projection.EQUIRECTANGULAR) {
      return equiImage;
    }

    const { [projection]: currentProjectionData } = this.projections
    const baseProjection = currentProjectionData.fn();
    const modifiedProjection = baseProjection.center([0, 0]);
    const { invert } = modifiedProjection;
    if (!invert) {
      return null;
    }

    const { width: sx, height: sy, data: sourceData } = equiImage;
    const [tx, ty] = currentProjectionData.bounds([sx, sy]);
    const target = new PNG({
      width: tx,
      height: ty,
    });
    const targetData = target.data;

    let i = 0;
    for (let y = (sy - ty) / 2; y < (ty + ((sy - ty) / 2)); y += 1) {
      for (let x = 0; x < sx; x += 1) {
        const projected = invert([x / sx * this.referenceWidth, y / sx * this.referenceWidth]) as [number, number];
        if (projected) {
          const [lambda, phi] = projected;
          if (!(lambda > 180 || lambda < -180 || phi > 90 || phi < -90)) {
            const q = ((90 - phi) / 180 * sy | 0) * sx + ((180 + lambda) / 360 * sx | 0) << 2;
            targetData[i] = sourceData[q];
            targetData[i + 1] = sourceData[q + 1];
            targetData[i + 2] = sourceData[q + 2];
            targetData[i + 3] = 255;
          }
        }
        i += 4;
      }
    }

    return target;
  }
}

