import {NextApiHandler} from 'next';
import * as fs from 'fs/promises';
import {PNG} from 'pngjs';
import {MapType, Projection, WaterType, WorldData} from '../../../utils/types';
import {Stats} from 'fs';
import sharp from 'sharp';
import {ProjectionServiceImpl} from '../../../backend/services/Projection.service';
import {FileSystemServiceImpl} from '../../../backend/services/FileSystem.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 = 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.replace('public/', '/');
}

enum LandType {
  MESA = 0,
  DESERT = 1,
  FOREST = 2,
  PLAINS = 3,
  ICE = 4,
  BEACH = 5,
  MUD = 6,
}

const rgb = (r: number, g: number, b: number) => {
  return (r << 16) + (g << 8) + b
}

const checkBiomeLandType = (biomePngData: Buffer, x: number, y: number, width: number) => {
  const dataIndex = ((y * width) + x) * 4;
  switch (rgb(
    biomePngData[dataIndex],
    biomePngData[dataIndex + 1],
    biomePngData[dataIndex + 2],
  )) {
  case 0xfa9418:
  case 0xffbc40:
    return LandType.DESERT;
  case 0x606060:
  case 0xd25f12:
  case 0xd94515:
  case 0xca8c65:
    return LandType.MESA;
  case 0x056621:
  case 0x0b6659:
  case 0x537b09:
  case 0x2c4205:
  case 0x628b17:
  case 0x507050:
  case 0x2d8e49:
  case 0x8ab33f:
  case 0x687942:
    return LandType.FOREST;
  case 0x596651:
  case 0x454f3e:
  case 0x338e81:
    // return LandType.MUD;
    // case 0xfade55:
    //   return LandType.BEACH;
  case 0xffffff:
  case 0xa0a0a0:
  case 0xfaf0c0:
  case 0x597d72:
  case 0x202070:
  case 0x202038:
  case 0x404090:
  case 0x163933:
    return LandType.ICE;
  case 0xfade55:
  case 0x8db360:
  case 0x07f9b2:
  case 0xbdb25f:
  case 0xa79d64:
  case 0xb5db88:
  case 0x000070:
  case 0x0000ff:
  case 0x0000ac:
  case 0x000090:
  case 0x000050:
  case 0x000040:
  default:
    break;
  }

  return LandType.PLAINS;
}

const isMountainBiome = (biomePngData: Buffer, x: number, y: number, width: number) => {
  const dataIndex = ((y * width) + x) * 4;
  switch (rgb(
    (biomePngData[dataIndex]),
    (biomePngData[dataIndex + 1]),
    (biomePngData[dataIndex + 2]),
  )) {
  case 0x606060:
  case 0xa0a0a0:
  case 0x507050:
  case 0x338e81:
  case 0x597d72:
    return true
  default:
    break
  }

  return false
}

const isHillBiome = (biomePngData: Buffer, x: number, y: number, width: number) => {
  const dataIndex = ((y * width) + x) * 4;
  switch (rgb(
    (biomePngData[dataIndex]),
    (biomePngData[dataIndex + 1]),
    (biomePngData[dataIndex + 2]),
  )) {
  case 0xd25f12:
  case 0x163933:
  case 0x2c4205:
  case 0x454f3e:
  case 0x687942:
    return true
  default:
    break
  }

  return false
}

const isWoodedBiome = (biomePngData: Buffer, x: number, y: number, width: number) => {
  const dataIndex = ((y * width) + x) * 4;
  switch (rgb(
    (biomePngData[dataIndex]),
    (biomePngData[dataIndex + 1]),
    (biomePngData[dataIndex + 2]),
  )) {
  case 0x056621:
  case 0x537b09:
  case 0x596651:
  case 0x2d8e49:
    return true;
  default:
    break
  }
  return false;
}

const index = (x: number, y: number, width: number) => {
  return ((y * width) + x) * 4;
}

const landThreshold = 0x80
const SHALLOW_WATER_THRESHOLD = 0xE0

const checkNeighboringLands = (waterMaskPngData: Buffer, x: number, y: number, width: number, height: number) => {
  const ny = y === 0 ? 0 : y - 1;
  const wx = (x + width - 1) % width;
  const ex = (x + 1) % width ;
  const sy = y === height - 1 ? height - 1 : y + 1;

  return {
    nw: waterMaskPngData[index(wx, ny, width)] < landThreshold,
    n: waterMaskPngData[index(x, ny, width)] < landThreshold,
    ne: waterMaskPngData[index(ex, ny, width)] < landThreshold,
    w: waterMaskPngData[index(wx, y, width)] < landThreshold,
    i: waterMaskPngData[index(x, y, width)],
    e: waterMaskPngData[index(ex, y, width)] < landThreshold,
    sw: waterMaskPngData[index(wx, sy, width)] < landThreshold,
    s: waterMaskPngData[index(x, sy, width)] < landThreshold,
    se: waterMaskPngData[index(ex, sy, width)] < landThreshold,
  };
}

const determineBackgroundTileType = (landFlags: ReturnType<typeof checkNeighboringLands>) => {
  const islandThreshold = 0xE0;
  if (landFlags.i < landThreshold) {
    return 1;                // 1
  }

  if (
    landFlags.i < islandThreshold
    && !(
      landFlags.nw
      || landFlags.n
      || landFlags.ne
      || landFlags.w
      || landFlags.e
      || landFlags.sw
      || landFlags.s
      || landFlags.se
    )
  ) {
    return 29;              // 29
  }

  let cursor = 0;
  if (landFlags.n) {
    cursor += 12;            // 12
    if (landFlags.e) {
      cursor += 12;          // 24
      if (landFlags.s) {
        cursor += 14;        // 38
        if (landFlags.w) {
          cursor -= 10;      // 28
        }
      } else if (landFlags.w) {
        cursor += 16;        // 40
      } else if (landFlags.sw) {
        cursor += 12;        // 36
      }
    } else if (landFlags.w) {
      cursor += 15;          // 27
      if (landFlags.s) {
        cursor += 12;        // 39
      } else if (landFlags.se) {
        cursor += 10;        // 37
      }
    } else if (landFlags.s) {
      cursor += 31;          // 43
    } else if (landFlags.se) {
      cursor += 8;           // 20
      if (landFlags.sw) {
        cursor += 12;        // 32
      }
    } else if (landFlags.sw) {
      cursor += 11;          // 23
    }
  } else if (landFlags.s) {
    cursor += 13;            // 13
    if (landFlags.e) {
      cursor += 12;          // 25
      if (landFlags.w) {
        cursor += 16;        // 41
      } else if (landFlags.nw) {
        cursor += 10;        // 35
      }
    } else if (landFlags.w) {
      cursor += 13;          // 26
      if (landFlags.ne) {
        cursor += 8;         // 34
      }
    } else if (landFlags.ne) {
      cursor += 8;           // 21
      if (landFlags.nw) {
        cursor += 20;        // 33
      }
    } else if (landFlags.nw) {
      cursor += 9;           // 22
    }
  } else if (landFlags.w) {
    cursor += 14;            // 14
    if (landFlags.e) {
      cursor += 28;          // 42
    } else if (landFlags.se) {
      cursor += 4;           // 18
      if (landFlags.ne) {
        cursor += 13;        // 31
      }
    } else if (landFlags.ne) {
      cursor += 5;           // 19
    }
  } else if (landFlags.e) {
    cursor += 15;            // 15
    if (landFlags.nw) {
      cursor += 1            // 16
      if (landFlags.sw) {
        cursor += 14;        // 30
      }
    } else if (landFlags.sw) {
      cursor += 2;           // 17
    }
  } else if (landFlags.ne) {
    cursor += 2;             // 2
    if (landFlags.nw) {
      cursor += 4;           // 6
      if (landFlags.sw) {
        cursor += 42;        // 48
        if (landFlags.se) {
          cursor -= 4;       // 44
        }
      } else if (landFlags.se) {
        cursor += 39;        // 45
      }
    } else if (landFlags.se) {
      cursor += 7;           // 9
      if (landFlags.sw) {
        cursor += 37;        // 46
      }
    } else if (landFlags.sw) {
      cursor += 8;           // 10
    }
  } else if (landFlags.nw) {
    cursor += 3;             // 3
    if (landFlags.sw) {
      cursor += 5;           // 8
      if (landFlags.se) {
        cursor += 39;        // 47
      }
    } else if (landFlags.se) {
      cursor += 8;           // 11
    }
  } else if (landFlags.sw) {
    cursor += 4;             // 4
    if (landFlags.se) {
      cursor += 3;           // 7
    }
  } else if (landFlags.se) {
    cursor += 5;             // 5
  }

  return cursor;
}

const generateWorldData = async (p: Projection, size: number, isWidthDimension: boolean) => {
  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[][],
  }

  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) {
      const isShallowWater = data[MapType.BATHYMETRY].data[i] >= SHALLOW_WATER_THRESHOLD;
      worldData.spriteWaterLayer[y][x] = isShallowWater ? WaterType.LIGHT : WaterType.DEFAULT;

      const landType = checkBiomeLandType(data[MapType.BIOME].data, x, y, dx);
      const tileType = determineBackgroundTileType(checkNeighboringLands(data[MapType.WATER_MASK].data, x, y, dx, dy));
      worldData.spriteBackgroundLayer[y][x] = ((landType * 60) + tileType);
      worldData.spriteForegroundLayer[y][x] = 0;

      if (tileType === 1) {
        const isMountain = data[MapType.ELEVATION].data[i] >= 0x40 || isMountainBiome(
          data[MapType.BIOME].data,
          x,
          y,
          dx
        );
        if (isMountain) {
          switch (landType) {
          case LandType.DESERT:
          case LandType.MESA:
            worldData.spriteForegroundLayer[y][x] = 796;
            break;
          case LandType.ICE:
            worldData.spriteForegroundLayer[y][x] = 799;
            break;
          case LandType.FOREST:
          case LandType.PLAINS:
          default:
            worldData.spriteForegroundLayer[y][x] = 797;
            break;
          }
        }

        const isHill = isHillBiome(data[MapType.BIOME].data, x, y, dx);
        if (isHill) {
          if (landType === LandType.ICE) {
            worldData.spriteForegroundLayer[y][x] = 903
          } else {
            worldData.spriteForegroundLayer[y][x] = 901
          }
        }

        const isWooded = isWoodedBiome(data[MapType.BIOME].data, x, y, dx);
        if (isWooded) {
          if (landType === LandType.ICE) {
            worldData.spriteForegroundLayer[y][x] = 906
          } else {
            worldData.spriteForegroundLayer[y][x] = 905
          }
        }
      }
      i += 4;
    }
  }

  return worldData;
}

const generateHandler: NextApiHandler = async (req, res) => {
  const {
    projection = Projection.EQUIRECTANGULAR,
    width,
    height,
  } = req.query
  let isWidthDimension: boolean;
  let size: number;
  if (width && Number.isFinite(Number(width))) {
    size = Number(width);
    isWidthDimension = true;
  } else if (height && Number.isFinite(Number(height))) {
    size = Number(height);
    isWidthDimension = false;
  } else {
    throw new Error('Unspecified width or height');
  }

  await FileSystemServiceImpl.ensureDirectory('public/generated');
  await FileSystemServiceImpl.ensureDirectory('public/generated/base');
  await FileSystemServiceImpl.ensureDirectory('public/generated/resized');
  await FileSystemServiceImpl.ensureDirectory('public/worlds');
  const baseDataImageUrls = await Promise.all(
    Object
      .values(MapType)
      .map(async (t: MapType) => generateProjectedBaseData(t, projection as Projection))
  );
  await Promise.all(
    Object
      .values(MapType)
      .map(async (t: MapType) => resizeBaseData(t, projection as Projection, size, isWidthDimension))
  );
  const worldData = await generateWorldData(projection as Projection, size, isWidthDimension);

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

export default generateHandler;
