// eslint-disable-next-line no-unused-vars
import Dexie, { Version } from "dexie";
import shortid from "shortid";
import { v4 as uuid } from "uuid";
import Case from "case";
import chunk from "lodash.chunk";

import blobToBuffer from "./helpers/blobToBuffer";
import { getGridDefaultInset } from "./helpers/grid";
import { createThumbnail, getImageOutline } from "./helpers/image";
import {
  AddStatesAction,
  EditStatesAction,
  RemoveStatesAction,
  SubtractFogAction,
  CutFogAction,
} from "./actions";
import { getDefaultMaps } from "./maps";
import { getDefaultTokens } from "./tokens";
import { Outline } from "./types/Outline";
import { Group, GroupContainer } from "./types/Group";
import cloneDeep from "lodash.clonedeep";

export type UpgradeEventHandler = (versionNumber: number) => void;

type VersionCallback = (
  version: Version,
  onUpgrade?: UpgradeEventHandler
) => void;

/**
 * Mapping of version number to their upgrade function
 */
export const versions: Record<number, VersionCallback> = {
  // v1.2.0
  1(v) {
    v.stores({
      maps: "id, owner",
      states: "mapId",
      tokens: "id, owner",
      user: "key",
    });
  },
  // v1.2.1 - Move from blob files to array buffers
  2(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(2);
      const maps = await Dexie.waitFor(tx.table("maps").toArray());
      let mapBuffers: Record<string, ArrayBuffer> = {};
      for (let map of maps) {
        mapBuffers[map.id] = await Dexie.waitFor(blobToBuffer(map.file));
      }
      return tx
        .table("maps")
        .toCollection()
        .modify((map) => {
          map.file = mapBuffers[map.id];
        });
    });
  },
  // v1.3.0 - Added new default tokens
  3(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(3);
      return tx
        .table("states")
        .toCollection()
        .modify((state) => {
          function mapTokenId(id: string) {
            switch (id) {
              case "__default-Axes":
                return "__default-Barbarian";
              case "__default-Bird":
                return "__default-Druid";
              case "__default-Book":
                return "__default-Wizard";
              case "__default-Crown":
                return "__default-Humanoid";
              case "__default-Dragon":
                return "__default-Dragon";
              case "__default-Eye":
                return "__default-Warlock";
              case "__default-Fist":
                return "__default-Monk";
              case "__default-Horse":
                return "__default-Fey";
              case "__default-Leaf":
                return "__default-Druid";
              case "__default-Lion":
                return "__default-Monstrosity";
              case "__default-Money":
                return "__default-Humanoid";
              case "__default-Moon":
                return "__default-Cleric";
              case "__default-Potion":
                return "__default-Sorcerer";
              case "__default-Shield":
                return "__default-Paladin";
              case "__default-Skull":
                return "__default-Undead";
              case "__default-Snake":
                return "__default-Beast";
              case "__default-Sun":
                return "__default-Cleric";
              case "__default-Swords":
                return "__default-Fighter";
              case "__default-Tree":
                return "__default-Plant";
              case "__default-Triangle":
                return "__default-Sorcerer";
              default:
                return "__default-Fighter";
            }
          }
          for (let stateId in state.tokens) {
            state.tokens[stateId].tokenId = mapTokenId(
              state.tokens[stateId].tokenId
            );
            state.tokens[stateId].lastEditedBy = "";
            state.tokens[stateId].rotation = 0;
          }
        });
    });
  },
  // v1.3.1 - Added show grid option
  4(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(4);
      return tx
        .table("maps")
        .toCollection()
        .modify((map) => {
          map.showGrid = false;
        });
    });
  },
  // v1.4.0 - Added fog subtraction
  5(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(5);
      return tx
        .table("states")
        .toCollection()
        .modify((state) => {
          for (let fogAction of state.fogDrawActions) {
            if (fogAction.type === "add" || fogAction.type === "edit") {
              for (let shape of fogAction.shapes) {
                shape.data.holes = [];
              }
            }
          }
        });
    });
  },
  // v1.4.2 - Added map resolutions
  6(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(6);
      return tx
        .table("maps")
        .toCollection()
        .modify((map) => {
          map.resolutions = {};
          map.quality = "original";
        });
    });
  },
  // v1.5.0 - Fixed default token rogue spelling
  7(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(7);
      return tx
        .table("states")
        .toCollection()
        .modify((state) => {
          for (let id in state.tokens) {
            if (state.tokens[id].tokenId === "__default-Rouge") {
              state.tokens[id].tokenId = "__default-Rogue";
            }
          }
        });
    });
  },
  // v1.5.0 - Added map snap to grid option
  8(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(8);
      return tx
        .table("maps")
        .toCollection()
        .modify((map) => {
          map.snapToGrid = true;
        });
    });
  },
  // v1.5.1 - Added lock, visibility and modified to tokens
  9(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(9);
      return tx
        .table("states")
        .toCollection()
        .modify((state) => {
          for (let id in state.tokens) {
            state.tokens[id].lastModifiedBy = state.tokens[id].lastEditedBy;
            delete state.tokens[id].lastEditedBy;
            state.tokens[id].lastModified = Date.now();
            state.tokens[id].locked = false;
            state.tokens[id].visible = true;
          }
        });
    });
  },
  // v1.5.1 - Added token prop category and remove isVehicle bool
  10(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(10);
      return tx
        .table("tokens")
        .toCollection()
        .modify((token) => {
          token.category = token.isVehicle ? "vehicle" : "character";
          delete token.isVehicle;
        });
    });
  },
  // v1.5.2 - Added automatic cache invalidation to maps
  11(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(11);
      return tx
        .table("maps")
        .toCollection()
        .modify((map) => {
          map.lastUsed = map.lastModified;
        });
    });
  },
  // v1.5.2 - Added automatic cache invalidation to tokens
  12(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(12);
      return tx
        .table("tokens")
        .toCollection()
        .modify((token) => {
          token.lastUsed = token.lastModified;
        });
    });
  },
  // v1.6.0 - Added map grouping and grid scale and offset
  13(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(13);
      return tx
        .table("maps")
        .toCollection()
        .modify((map) => {
          map.group = "";
          map.grid = {
            size: { x: map.gridX, y: map.gridY },
            inset: getGridDefaultInset(
              { size: { x: map.gridX, y: map.gridY }, type: "square" },
              map.width,
              map.height
            ),
            type: "square",
          };
          delete map.gridX;
          delete map.gridY;
          delete map.gridType;
        });
    });
  },
  // v1.6.0 - Added token grouping
  14(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(14);
      return tx
        .table("tokens")
        .toCollection()
        .modify((token) => {
          token.group = "";
        });
    });
  },
  // v1.6.1 - Added width and height to tokens
  15(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(15);
      const tokens = await Dexie.waitFor(tx.table("tokens").toArray());
      let tokenSizes: Record<string, { width: number; height: number }> = {};
      for (let token of tokens) {
        const url = URL.createObjectURL(new Blob([token.file]));
        let image = new Image();
        tokenSizes[token.id] = await Dexie.waitFor(
          new Promise((resolve) => {
            image.onload = () => {
              resolve({ width: image.width, height: image.height });
            };
            image.src = url;
          })
        );
      }
      return tx
        .table("tokens")
        .toCollection()
        .modify((token) => {
          token.width = tokenSizes[token.id].width;
          token.height = tokenSizes[token.id].height;
        });
    });
  },
  // v1.7.0 - Added note tool
  16(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(16);
      return tx
        .table("states")
        .toCollection()
        .modify((state) => {
          state.notes = {};
          state.editFlags = [...state.editFlags, "notes"];
        });
    });
  },
  // 1.7.0 (hotfix) - Optimized fog shape edits to only include needed data
  17(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(17);
      return tx
        .table("states")
        .toCollection()
        .modify((state) => {
          for (let i = 0; i < state.fogDrawActions.length; i++) {
            const action = state.fogDrawActions[i];
            if (action && action.type === "edit") {
              for (let j = 0; j < action.shapes.length; j++) {
                const shape = action.shapes[j];
                const temp = { ...shape };
                state.fogDrawActions[i].shapes[j] = {
                  id: temp.id,
                  visible: temp.visible,
                };
              }
            }
          }
        });
    });
  },
  // 1.8.0 - Added note text only mode, converted draw and fog representations
  18(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(18);
      return tx
        .table("states")
        .toCollection()
        .modify((state) => {
          for (let id in state.notes) {
            state.notes[id].textOnly = false;
          }

          state.drawShapes = convertOldActionsToShapes(
            state.mapDrawActions,
            state.mapDrawActionIndex
          );
          state.fogShapes = convertOldActionsToShapes(
            state.fogDrawActions,
            state.fogDrawActionIndex
          );

          delete state.mapDrawActions;
          delete state.mapDrawActionIndex;
          delete state.fogDrawActions;
          delete state.fogDrawActionIndex;
        });
    });
  },
  // 1.8.0 - Add thumbnail to maps and add measurement to grid
  19(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(19);
      const userId = (await Dexie.waitFor(tx.table("user").get("userId")))
        .value;
      const maps = await Dexie.waitFor(tx.table("maps").toArray());
      const thumbnails: Record<string, any> = {};
      for (let map of maps) {
        try {
          if (map.owner === userId) {
            thumbnails[map.id] = await createDataThumbnail(map);
          }
        } catch {}
      }
      return tx
        .table("maps")
        .toCollection()
        .modify((map) => {
          map.thumbnail = thumbnails[map.id];
          map.grid.measurement = { type: "chebyshev", scale: "5ft" };
        });
    });
  },
  // 1.8.0 - Add thumbnail to tokens
  20(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(20);
      const userId = (await Dexie.waitFor(tx.table("user").get("userId")))
        .value;
      const tokens = await Dexie.waitFor(tx.table("tokens").toArray());
      const thumbnails: Record<string, any> = {};
      for (let token of tokens) {
        try {
          if (token.owner === userId) {
            thumbnails[token.id] = await createDataThumbnail(token);
          }
        } catch {}
      }
      return tx
        .table("tokens")
        .toCollection()
        .modify((token) => {
          token.thumbnail = thumbnails[token.id];
        });
    });
  },
  // 1.8.0 - Upgrade for Dexie.Observable
  21(v) {
    v.stores({});
  },
  // v1.8.1 - Shorten fog shape ids
  22(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(22);
      return tx
        .table("states")
        .toCollection()
        .modify((state) => {
          for (let id of Object.keys(state.fogShapes)) {
            const newId = shortid.generate();
            state.fogShapes[newId] = state.fogShapes[id];
            state.fogShapes[newId].id = newId;
            delete state.fogShapes[id];
          }
        });
    });
  },
  // v1.9.0 - Add outlines to tokens
  23(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(23);
      const tokens = await Dexie.waitFor(tx.table("tokens").toArray());
      const tokenOutlines = await Dexie.waitFor(
        Promise.all(tokens.map(createDataOutline))
      );

      return tx
        .table("tokens")
        .toCollection()
        .modify((token) => {
          const tokenOutline = tokenOutlines.find((el) => el.id === token.id);
          if (tokenOutline) {
            token.outline = tokenOutline.outline;
          } else {
            token.outline = {
              type: "rect",
              width: token.width,
              height: token.height,
              x: 0,
              y: 0,
            };
          }
        });
    });
  },
  // v1.9.0 - Move map assets into new table
  24(v, onUpgrade) {
    v.stores({ assets: "id, owner" }).upgrade(async (tx) => {
      onUpgrade?.(24);

      const primaryKeys = await Dexie.waitFor(
        tx.table("maps").toCollection().primaryKeys()
      );
      const keyChunks = chunk(primaryKeys, 4);

      for (let keys of keyChunks) {
        let assets = [];
        let maps = await Dexie.waitFor(tx.table("maps").bulkGet(keys));
        while (maps.length > 0) {
          const map = maps.pop();
          assets.push({
            id: uuid(),
            owner: map.owner,
            file: map.file,
            width: map.width,
            height: map.height,
            mime: "",
            prevId: map.id,
            prevType: "map",
          });

          for (let resolution in map.resolutions) {
            const mapRes = map.resolutions[resolution];
            assets.push({
              id: uuid(),
              owner: map.owner,
              file: mapRes.file,
              width: mapRes.width,
              height: mapRes.height,
              mime: "",
              prevId: map.id,
              prevType: "mapResolution",
              resolution,
            });
          }
          if (map.thumbnail) {
            assets.push({
              id: uuid(),
              owner: map.owner,
              file: map.thumbnail.file,
              width: map.thumbnail.width,
              height: map.thumbnail.height,
              mime: "",
              prevId: map.id,
              prevType: "mapThumbnail",
            });
          }
        }
        await tx.table("assets").bulkAdd(assets);
      }
    });
  },
  // v1.9.0 - Move token assets into new table
  25(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(25);

      const primaryKeys = await Dexie.waitFor(
        tx.table("tokens").toCollection().primaryKeys()
      );
      const keyChunks = chunk(primaryKeys, 4);

      for (let keys of keyChunks) {
        let assets = [];
        let tokens = await Dexie.waitFor(tx.table("tokens").bulkGet(keys));
        while (tokens.length > 0) {
          let token = tokens.pop();
          assets.push({
            id: uuid(),
            owner: token.owner,
            file: token.file,
            width: token.width,
            height: token.height,
            mime: "",
            prevId: token.id,
            prevType: "token",
          });
          if (token.thumbnail) {
            assets.push({
              id: uuid(),
              owner: token.owner,
              file: token.thumbnail.file,
              width: token.thumbnail.width,
              height: token.thumbnail.height,
              mime: "",
              prevId: token.id,
              prevType: "tokenThumbnail",
            });
          }
        }
        await tx.table("assets").bulkAdd(assets);
      }
    });
  },
  // v1.9.0 - Create foreign keys for assets
  26(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(26);

      let mapUpdates: Record<string, any> = {};
      let tokenUpdates: Record<string, any> = {};

      const primaryKeys = await Dexie.waitFor(
        tx.table("assets").toCollection().primaryKeys()
      );
      const keyChunks = chunk(primaryKeys, 4);

      for (let keys of keyChunks) {
        let assets = await Dexie.waitFor(tx.table("assets").bulkGet(keys));
        while (assets.length > 0) {
          const asset = assets.pop();
          const { prevId, id, prevType, resolution } = asset;
          if (prevType === "token" || prevType === "tokenThumbnail") {
            if (!(prevId in tokenUpdates)) {
              tokenUpdates[prevId] = {};
            }
          } else {
            if (!(prevId in mapUpdates)) {
              mapUpdates[prevId] = {};
            }
          }

          if (prevType === "map") {
            mapUpdates[prevId].file = id;
          } else if (prevType === "token") {
            tokenUpdates[prevId].file = id;
          } else if (prevType === "mapThumbnail") {
            mapUpdates[prevId].thumbnail = id;
          } else if (prevType === "tokenThumbnail") {
            tokenUpdates[prevId].thumbnail = id;
          } else if (prevType === "mapResolution") {
            mapUpdates[prevId][resolution] = id;
          }
        }
      }

      await tx
        .table("maps")
        .toCollection()
        .modify((map) => {
          if (map.id in mapUpdates) {
            for (let key in mapUpdates[map.id]) {
              map[key] = mapUpdates[map.id][key];
            }
          }
          delete map.resolutions;
        });
      await tx
        .table("tokens")
        .toCollection()
        .modify((token) => {
          if (token.id in tokenUpdates) {
            for (let key in tokenUpdates[token.id]) {
              token[key] = tokenUpdates[token.id][key];
            }
          }
        });
    });
  },
  // v1.9.0 - Remove asset migration helpers
  27(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(27);
      tx.table("assets").toCollection().modify({
        prevId: undefined,
        prevType: undefined,
        resolution: undefined,
      });
    });
  },
  // v1.9.0 - Remap map resolution assets
  28(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(28);
      tx.table("maps")
        .toCollection()
        .modify((map) => {
          const resolutions = ["low", "medium", "high", "ultra"];
          map.resolutions = {};
          for (let res of resolutions) {
            if (res in map) {
              map.resolutions[res] = map[res];
              delete map[res];
            }
          }
          delete map.lastUsed;
        });
    });
  },
  // v1.9.0 - Move tokens to use more defaults
  29(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(29);
      tx.table("tokens")
        .toCollection()
        .modify(async (token) => {
          token.defaultCategory = token.category;
          delete token.category;
          token.defaultLabel = "";
          delete token.lastUsed;
        });
    });
  },
  // v1.9.0 - Move tokens to use more defaults and add token outline to token states
  30(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(30);
      const tokens = await Dexie.waitFor(tx.table("tokens").toArray());
      tx.table("states")
        .toCollection()
        .modify((state) => {
          for (let id in state.tokens) {
            if (!state.tokens[id]?.tokenId) {
              delete state.tokens[id];
              continue;
            }
            if (!state.tokens[id].tokenId.startsWith("__default")) {
              const token = tokens.find(
                (token) => token.id === state.tokens[id].tokenId
              );
              if (token) {
                state.tokens[id].category = token.defaultCategory;
                state.tokens[id].file = token.file;
                state.tokens[id].type = "file";
                state.tokens[id].outline = token.outline;
                state.tokens[id].width = token.width;
                state.tokens[id].height = token.height;
                state.tokens[id].owner = token.owner;
              } else {
                state.tokens[id].category = "character";
                state.tokens[id].type = "file";
                state.tokens[id].file = "";
                state.tokens[id].outline = {
                  type: "rect",
                  width: 256,
                  height: 256,
                  x: 0,
                  y: 0,
                };
                state.tokens[id].width = 256;
                state.tokens[id].height = 256;
              }
            } else {
              state.tokens[id].category = "character";
              state.tokens[id].type = "default";
              state.tokens[id].key = Case.camel(
                state.tokens[id].tokenId.slice(10)
              );
              state.tokens[id].outline = {
                type: "circle",
                x: 128,
                y: 128,
                radius: 128,
              };
              state.tokens[id].width = 256;
              state.tokens[id].height = 256;
            }
          }
        });
    });
  },
  // v1.9.0 - Remove maps not owned by user as cache is now done on the asset level
  31(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(31);
      const userId = (await Dexie.waitFor(tx.table("user").get("userId")))
        ?.value;
      if (userId) {
        tx.table("maps").where("owner").notEqual(userId).delete();
      }
    });
  },
  // v1.9.0 - Remove tokens not owned by user as cache is now done on the asset level
  32(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(32);
      const userId = (await Dexie.waitFor(tx.table("user").get("userId")))
        ?.value;
      if (userId) {
        tx.table("tokens").where("owner").notEqual(userId).delete();
      }
    });
  },
  // v1.9.0 - Store default maps and tokens in db
  33(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(33);
      const userId = (await Dexie.waitFor(tx.table("user").get("userId")))
        ?.value;
      if (!userId) {
        return;
      }
      const { maps } = getDefaultMaps(userId);
      tx.table("maps").bulkAdd(maps);
      const tokens = getDefaultTokens(userId);
      tx.table("tokens").bulkAdd(tokens);
    });
  },
  // v1.9.0 - Add new group table
  34(v, onUpgrade) {
    v.stores({ groups: "id" }).upgrade(async (tx) => {
      onUpgrade?.(34);
      function groupItems(items: { id: string; group: string }[]) {
        let groups: Group[] = [];
        let subGroups: Record<string, GroupContainer> = {};
        for (let item of items) {
          if (!item.group) {
            groups.push({ id: item.id, type: "item" });
          } else if (item.group in subGroups) {
            subGroups[item.group].items.push({ id: item.id, type: "item" });
          } else {
            subGroups[item.group] = {
              id: uuid(),
              type: "group",
              name: item.group,
              items: [{ id: item.id, type: "item" }],
            };
          }
        }
        groups.push(...Object.values(subGroups));
        return groups;
      }

      let maps = await Dexie.waitFor(tx.table("maps").toArray());
      maps = maps.sort((a, b) => b.created - a.created);
      const mapGroupItems = groupItems(maps);
      tx.table("groups").add({ id: "maps", items: mapGroupItems });

      let tokens = await Dexie.waitFor(tx.table("tokens").toArray());
      tokens = tokens.sort((a, b) => b.created - a.created);
      const tokenGroupItems = groupItems(tokens);
      tx.table("groups").add({ id: "tokens", items: tokenGroupItems });
    });
  },
  // v1.9.0 - Remove map and token group in respective tables
  35(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(35);
      tx.table("maps")
        .toCollection()
        .modify((map) => {
          delete map.group;
        });
      tx.table("tokens")
        .toCollection()
        .modify((token) => {
          delete token.group;
        });
    });
  },
  36(v) {
    v.stores({
      _changes: null,
      _intercomm: null,
      _syncNodes: null,
      _uncommittedChanges: null,
    });
  },
  // v1.10.0 - Add rotation to notes
  37(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(37);
      tx.table("states")
        .toCollection()
        .modify((state) => {
          for (let id in state.notes) {
            state.notes[id].rotation = 0;
          }
        });
    });
  },
  // v1.10.0 - Delete groups again
  38(v, onUpgrade) {
    v.stores({}).upgrade(async (tx) => {
      onUpgrade?.(38);
      tx.table("maps")
        .toCollection()
        .modify((map) => {
          delete map.group;
        });
      tx.table("tokens")
        .toCollection()
        .modify((token) => {
          delete token.group;
        });
    });
  },
  // v1.10.0 - Rename drawShapes and fogShapes in state
  39(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(39);
      tx.table("states")
        .toCollection()
        .modify((state) => {
          state.drawings = state.drawShapes;
          state.fogs = state.fogShapes;

          delete state.drawShapes;
          delete state.fogShapes;
        });
    });
  },
  // v1.10.0 (patch 1) - Rename drawShapes and fogShapes in state again (some people's didn't work)
  40(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(40);
      tx.table("states")
        .toCollection()
        .modify((state) => {
          if (state.drawShapes) {
            state.drawings = cloneDeep(state.drawShapes);
            delete state.drawShapes;
          }
          if (state.fogShapes) {
            state.fogs = cloneDeep(state.fogShapes);
            delete state.fogShapes;
          }
        });
    });
  },
  // v1.10.0 (patch 2) - Rename drawShapes and fogShapes in state again again (some people's still didn't work)
  41(v, onUpgrade) {
    v.stores({}).upgrade((tx) => {
      onUpgrade?.(41);
      return tx
        .table("states")
        .toCollection()
        .modify((state) => {
          if (state.drawShapes) {
            state.drawings = cloneDeep(state.drawShapes);
            delete state.drawShapes;
          }
          if (state.fogShapes) {
            state.fogs = cloneDeep(state.fogShapes);
            delete state.fogShapes;
          }
        });
    });
  },
};

export const latestVersion = 41;

/**
 * Load versions onto a database up to a specific version number
 * @param {Dexie} db
 * @param {number=} upTo version number to load up to, latest version if undefined
 * @param {UpgradeEventHandler=} onUpgrade
 */
export function loadVersions(
  db: Dexie,
  upTo: number | undefined = latestVersion,
  onUpgrade: UpgradeEventHandler | undefined
) {
  for (let versionNumber = 1; versionNumber <= upTo; versionNumber++) {
    versions[versionNumber](db.version(versionNumber), onUpgrade);
  }
}

/**
 * Convert from the previous representation of actions (1.7.0) to the new representation (1.8.0)
 * and combine into shapes
 */
function convertOldActionsToShapes(actions: any[], actionIndex: number) {
  let newShapes = {};
  for (let i = 0; i <= actionIndex; i++) {
    const action = actions[i];
    if (!action) {
      continue;
    }
    let newAction;
    if (action.shapes) {
      if (action.type === "add") {
        newAction = new AddStatesAction(action.shapes);
      } else if (action.type === "edit") {
        newAction = new EditStatesAction(action.shapes);
      } else if (action.type === "remove") {
        newAction = new RemoveStatesAction(action.shapes);
      } else if (action.type === "subtract") {
        newAction = new SubtractFogAction(action.shapes);
      } else if (action.type === "cut") {
        newAction = new CutFogAction(action.shapes);
      }
    } else if (action.type === "remove" && action.shapeIds) {
      newAction = new RemoveStatesAction(action.shapeIds);
    }

    if (newAction) {
      newShapes = newAction.execute(newShapes);
    }
  }
  return newShapes;
}

// Helper to create a thumbnail for a file in a db
async function createDataThumbnail(data: any): Promise<{
  file: Uint8Array;
  width: number;
  height: number;
  type: "file";
  id: "thumbnail";
}> {
  let url: string;
  if (data?.resolutions?.low?.file) {
    url = URL.createObjectURL(new Blob([data.resolutions.low.file]));
  } else {
    url = URL.createObjectURL(new Blob([data.file]));
  }
  return await Dexie.waitFor(
    new Promise((resolve) => {
      let image = new Image();
      image.onload = async () => {
        const thumbnail = await createThumbnail(image, "");
        if (thumbnail) {
          resolve({
            file: thumbnail.file,
            width: thumbnail.width,
            height: thumbnail.height,
            type: "file",
            id: "thumbnail",
          });
        }
      };
      image.src = url;
    }),
    60000 * 10 // 10 minute timeout
  );
}

async function createDataOutline(
  data: any
): Promise<{ id: string; outline: Outline }> {
  const url = URL.createObjectURL(new Blob([data.file]));
  return await Dexie.waitFor(
    new Promise((resolve) => {
      let image = new Image();
      image.onload = async () => {
        resolve({ id: data.id, outline: getImageOutline(image) });
      };
      image.onerror = () => {
        resolve({
          id: data.id,
          outline: {
            type: "rect",
            width: data.width,
            height: data.height,
            x: 0,
            y: 0,
          },
        });
      };
      image.src = url;
    })
  );
}
