import {NextApiHandler} from 'next';
import * as fs from 'fs/promises';
import {PNG} from 'pngjs';
import {MapType, Projection, WorldData} from '../../../utils/types';
import {Stats} from 'fs';
import sharp from 'sharp';
import ProjectionService, {ProjectionServiceImpl} from '../../../backend/services/Projection.service';
import {FileSystemServiceImpl} from '../../../backend/services/FileSystem.service';
import TileService, {TileServiceImpl} from '../../../backend/services/Tile.service';

const generateProjectedBaseData = async (t: MapType, p: Projection) => {
  const destPath = `public/generated/base/${p}-${t}.png`;
  let stat: Stats | undefined;
  let shouldGenerateFile = false;
  try {
    stat = await fs.stat(destPath);
    shouldGenerateFile = !(stat && stat.isFile());
  } catch (errRaw) {
    const err = errRaw as { code: string };
    if (err.code !== 'ENOENT') {
      throw err;
    }
    shouldGenerateFile = true;
  }

  if (shouldGenerateFile) {
    const inputBuffer = await fs.readFile(`src/assets/data/000/${t}.png`)
    const inputPng = PNG.sync.read(inputBuffer);
    const projectionService: ProjectionService = new ProjectionServiceImpl()
    const outputPng = projectionService.project(inputPng, p) as PNG;
    const outputBuffer = PNG.sync.write(outputPng);
    await fs.writeFile(destPath, outputBuffer);
  }

  return destPath.replace('public/', '/');
}

const getResizeKernel = (t: MapType) => {
  switch (t) {
  case MapType.BIOME:
  case MapType.BATHYMETRY:
    return 'nearest';
  case MapType.WATER_MASK:
    return 'lanczos2';
  default:
    break;
  }
  return 'cubic';
}

const resizeBaseData = async (t: MapType, p: Projection, size: number, isWidthDimension: boolean) => {
  const destPath = `public/generated/resized/${isWidthDimension ? 'w' : 'h'}-${size}-${p}-${t}.png`;
  let resizeChain = sharp(`public/generated/base/${p}-${t}.png`)
    .resize({
      [isWidthDimension ? 'width' : 'height']: size,
      kernel: getResizeKernel(t),
    });

  if (t === MapType.WATER_MASK) {
    resizeChain = resizeChain.sharpen()
  }

  const buffer = await resizeChain
    .png()
    .toBuffer();

  await fs.writeFile(destPath, buffer);
  return destPath;
}

const generateWorldData = async (p: Projection, size: number, isWidthDimension: boolean) => {
  await FileSystemServiceImpl.ensureDirectory('public/generated/resized');
  const tempResizedFiles = await Promise.all(
    Object
      .values(MapType)
      .map(async (t: MapType) => resizeBaseData(t, p, size, isWidthDimension))
  );

  const mapTypeBuffers = await Promise.all(Object.values(MapType).map(async (t: MapType) => {
    return {
      mapType: t,
      buffer: await fs.readFile(`public/generated/resized/${isWidthDimension ? 'w' : 'h'}-${size}-${p}-${t}.png`),
    };
  }))

  const data = mapTypeBuffers.reduce<Record<MapType, PNG>>(
    (theData, mapTypeBuffer) => {
      return {
        ...theData,
        [mapTypeBuffer.mapType]: PNG.sync.read(mapTypeBuffer.buffer),
      }
    },
    {} as Record<MapType, PNG>
  );

  const dx = data[MapType.WATER_MASK].width;
  const dy = data[MapType.WATER_MASK].height;
  const worldData: WorldData = {
    version: '1.8.0.4',
    musicCategory: 0,
    width: dx,
    height: dy,
    spriteWaterLayer: [] as number[][],
    spriteBackgroundLayer: [] as number[][],
    spriteForegroundLayer: [] as number[][],
    connections: [] as number[][],
    tileTypes: [] as number[][],
    vehicleBoundaries: [] as number[][],
    stages: [] as number[][],
    warps: [] as number[][],
    vehicles: [] as number[][],
    initialItems: [] as number[][],
  }

  const tileService: TileService = new TileServiceImpl(
    data[MapType.BIOME].data,
    data[MapType.ELEVATION].data,
    data[MapType.WATER_MASK].data,
    data[MapType.BATHYMETRY].data
  )

  let i = 0;
  for (let y = 0; y < dy; y += 1) {
    worldData.spriteWaterLayer[y] = [];
    worldData.spriteBackgroundLayer[y] = [];
    worldData.spriteForegroundLayer[y] = [];
    for (let x = 0; x < dx; x += 1) {
      worldData.spriteWaterLayer[y][x] = tileService.determineWaterBackgroundTileType(x, y, dx);

      const tileType = tileService.determineBackgroundTileType(x, y, dx, dy);
      worldData.spriteBackgroundLayer[y][x] = tileType;
      worldData.spriteForegroundLayer[y][x] = tileService.determineForegroundTileType(tileType, x, y, dx);
      i += 4;
    }
  }

  await Promise.all(tempResizedFiles.map(f => fs.unlink(f)));
  await fs.rmdir('public/generated/resized');
  return worldData;
}

const ensureDirectories = async () => {
  await FileSystemServiceImpl.ensureDirectory('public/generated');
  await FileSystemServiceImpl.ensureDirectory('public/generated/base');
}

const determineDimension = (width: string, height: string) => {
  if (width && Number.isFinite(Number(width))) {
    return {
      size: Number(width),
      isWidthDimension: true,
    }
  }
  if (height && Number.isFinite(Number(height))) {
    return {
      size: Number(height),
      isWidthDimension: false,
    }
  }
  throw new Error('Unspecified width or height');
}

const generateHandler: NextApiHandler = async (req, res) => {
  const {
    projection = Projection.EQUIRECTANGULAR,
    width,
    height,
    bounds = '-180,90;180,-90',
  } = req.query
  const { size, isWidthDimension } = determineDimension(
    Array.isArray(width) ? width[0] : width,
    Array.isArray(height) ? height[0] : height,
  );

  await ensureDirectories();
  const baseDataImageUrls = await Promise.all(
    Object
      .values(MapType)
      .map(async (t: MapType) => generateProjectedBaseData(t, projection as Projection))
  );

  const worldData = await generateWorldData(projection as Projection, size, isWidthDimension);

  res.json({
    baseDataImageUrls,
    worldData,
  });
}

export default generateHandler;
