// NOTE: It's super not clear how much of this should be in rust vs
//       javascript. Dealing with the level code is really nice in javascript
//       but there's a bunch of weird stuff that really feels lower level.
import { load_texture } from "./assets";
import { Texture, spr, use_texture } from "./graphics";
import { load_string } from "./io";
import { log } from "./log";

// TODO: Use io-ts? YIKES.

export interface Tile {
  px: [number, number];
  src: [number, number];
  f: number;
  t: number;
  a: number;
}

export interface TileSet {
  id: number;
  texture: Texture;
  tags: Map<string, number[]>;
}

export interface TileLayer {
  type: "tile";
  tileset: TileSet;
  grid_size: number;
  offset: [number, number];
  tiles: Tile[];
}

export interface Entity {
  type: string;
  id: string;
  px: [number, number];
  bounds: [number, number];
  // TODO: More props here.
}

export interface EntityLayer {
  type: "entity";
  entities: Entity[];
}

export interface Level {
  iid: string;
  world_x: number;
  world_y: number;
  width: number;
  height: number;
  cw: number;
  ch: number;
  tile_layers: TileLayer[];
  entities: Entity[];
  values: { [key: string]: number[] };
  neighbors: {
    n: string | null;
    s: string | null;
    e: string | null;
    w: string | null;
  };
}

export interface World {
  levels: Level[];
  level_map: Map<string, Level>;
  tilesets: Map<number, TileSet>;
  current_level: Level;
}

interface LDTKTilesetDef {
  uid: number;
  relPath: string;
  enumTags: { enumValueId: string; tileIds: number[] }[];
}

async function load_tileset(def: LDTKTilesetDef): Promise<TileSet> {
  let relPath = def.relPath as string;
  if (relPath.endsWith(".aseprite")) {
    // Whoops let's load the export instead?
    relPath = relPath.substring(0, relPath.length - 8) + "png";
  }
  let texture = await load_texture(relPath);

  let tags = new Map();
  for (const et of def.enumTags) {
    tags.set(et.enumValueId, et.tileIds);
  }

  log(
    "map_load",
    "Loaded tileset",
    def.uid,
    "from",
    relPath,
    "as ID",
    texture.id()
  );
  return { id: def.uid, texture, tags };
}

interface LDTKTile {
  px: [number, number];
  src: [number, number];
  f: number;
  t: number;
  a: number;
}

interface LDTKTileLayerInstance {
  __type: "Tiles" | "IntGrid";
  __identifier: string;
  __gridSize: number;
  __pxTotalOffsetX: number;
  __pxTotalOffsetY: number;
  __tilesetDefUid: number;

  autoLayerTiles: LDTKTile[];
  gridTiles: LDTKTile[];
}

function load_tile_layer_instance(
  tile_sets: Map<number, TileSet>,
  li: LDTKTileLayerInstance
): TileLayer | undefined {
  const tiles = li.autoLayerTiles.concat(li.gridTiles);
  if (tiles.length == 0) {
    return;
  }

  const tileset = tile_sets.get(li.__tilesetDefUid);
  if (!tileset) {
    throw new Error("Unable to find texture!!! " + li.__tilesetDefUid);
  }

  return {
    type: "tile",
    tileset,
    grid_size: li.__gridSize,
    offset: [li.__pxTotalOffsetX, li.__pxTotalOffsetY],
    tiles,
  };
}

interface LDTKEntityLayerInstance {
  __type: "Entities";
  entityInstances: {
    __identifier: string;
    iid: string;
    width: number;
    height: number;
    px: [number, number];
  }[];
}

function load_entity_layer_instance(li: LDTKEntityLayerInstance): Entity[] {
  return li.entityInstances.map((ei) => {
    return {
      type: ei.__identifier,
      id: ei.iid,
      px: ei.px,
      bounds: [ei.width, ei.height],
    };
  });
}

interface LDTKIntGridLayerInstance {
  __type: "IntGrid";
  __identifier: string;
  __cWid: number;
  __cHei: number;
  intGridCsv: number[];
}

type LDTKLayerInstance =
  | LDTKTileLayerInstance
  | LDTKEntityLayerInstance
  | LDTKIntGridLayerInstance;

function is_tile_layer_instance(
  x: LDTKLayerInstance
): x is LDTKTileLayerInstance {
  return x.__type == "Tiles" || x.__type == "IntGrid";
}

function is_entity_layer_instance(
  x: LDTKLayerInstance
): x is LDTKEntityLayerInstance {
  return x.__type == "Entities";
}

function is_intgrid_layer_instance(
  x: LDTKLayerInstance
): x is LDTKIntGridLayerInstance {
  return x.__type == "IntGrid";
}

type LDTKLevel = {
  iid: string;
  worldX: number;
  worldY: number;
  pxWid: number;
  pxHei: number;
  layerInstances: LDTKLayerInstance[];
  __neighbours: { levelIid: string; dir: "n" | "s" | "e" | "w" }[];
};

function load_level(tile_sets: Map<number, TileSet>, def: LDTKLevel): Level {
  const result: Level = {
    iid: def.iid,
    world_x: def.worldX,
    world_y: def.worldY,
    width: def.pxWid,
    height: def.pxHei,
    cw: 0,
    ch: 0,
    tile_layers: [],
    entities: [],
    values: {},
    neighbors: { n: null, s: null, e: null, w: null },
  };

  for (const li of def.layerInstances) {
    if (is_tile_layer_instance(li)) {
      const tli = load_tile_layer_instance(tile_sets, li);
      if (tli) {
        result.tile_layers.push(tli);
      }
    }

    if (is_entity_layer_instance(li)) {
      // TODO: Why would I support multiple entity layers?
      result.entities = load_entity_layer_instance(li);
    }

    if (is_intgrid_layer_instance(li)) {
      result.cw = li.__cWid;
      result.ch = li.__cHei;
      result.values[li.__identifier] = li.intGridCsv;
    }
  }

  result.tile_layers = result.tile_layers.reverse();
  const neighbors = result.neighbors;
  for (const n of def.__neighbours) {
    if (n.dir == "n") {
      neighbors.n = n.levelIid;
    } else if (n.dir == "s") {
      neighbors.s = n.levelIid;
    } else if (n.dir == "e") {
      neighbors.e = n.levelIid;
    } else if (n.dir == "w") {
      neighbors.w = n.levelIid;
    }
  }
  return result;
}

interface LDTKMap {
  __header__: {
    fileType: "LDtk Project JSON";
    app: "LDtk";
    doc: "https://ldtk.io/json";
    schema: "https://ldtk.io/files/JSON_SCHEMA.json";
    appAuthor: "Sebastien 'deepnight' Benard";
    appVersion: "1.3.3";
    url: "https://ldtk.io";
  };
  defs: {
    tilesets: LDTKTilesetDef[];
  };
  levels: LDTKLevel[];
}

function is_ldtk_map(map: unknown): map is LDTKMap {
  if (
    map instanceof Object &&
    "__header__" in map &&
    map.__header__ instanceof Object
  ) {
    const header = map.__header__;
    if ("fileType" in header && header.fileType == "LDtk Project JSON") {
      return true;
    }
  }

  return false;
}

export async function load_world(path: string): Promise<World> {
  log("map_load", "Loading map:", path);
  const blob = await load_string(path);
  const map = JSON.parse(blob);

  if (!is_ldtk_map(map)) {
    throw new Error("Map does not appear to be an LDTK level");
  }

  const tilesets = new Map<number, TileSet>();
  let loaded_tilesets = await Promise.all(
    map.defs.tilesets
      .filter((def) => def.relPath != null)
      .map((def) => load_tileset(def))
  );
  for (const ts of loaded_tilesets) {
    tilesets.set(ts.id, ts);
  }

  const levels = map.levels.map((l: any) => load_level(tilesets, l));
  const level_map = new Map();
  for (const level of levels) {
    level_map.set(level.iid, level);
  }

  const current_level = levels.find((l) => l.world_x == 0 && l.world_y == 0);
  if (!current_level) {
    throw new Error("UNABLE TO FIND LEVEL AT 0,0: CANNOT START");
  }

  return { levels, level_map, tilesets, current_level };
}

export function has_collision(level: Level, cx: number, cy: number): boolean {
  if (!level) return true;
  if (cx < 0 || cx >= level.cw) return false;
  if (cy < 0 || cy >= level.ch) return false;

  const values = level.values.collisions;
  return values[cy * level.cw + cx] != 0; // TODO: MAGIC NUMBER?
}

export function draw_level(
  level: Level,
  offset_x: number = 0,
  offset_y: number = 0
) {
  for (const layer of level.tile_layers) {
    use_texture(layer.tileset.texture);

    let [ofx, ofy] = layer.offset;
    ofx += offset_x;
    ofy += offset_y;
    for (const tile of layer.tiles) {
      // TODO: Flip and whatnot.
      spr(
        tile.px[0] + ofx,
        tile.px[1] + ofy,
        layer.grid_size,
        layer.grid_size,
        tile.src[0],
        tile.src[1]
      );
    }
  }
}
