const PACKAGE_VERSION = "1.0.6";const REPOSITORY_URL = "https://github.com/ITCase/itcase-map";

import { produce } from 'immer';
import { z } from 'zod';
import { v4 } from 'uuid';
import PF from 'pathfinding';
import 'dom-to-image';
import 'file-saver';
import chroma from 'chroma-js';

class CoordsUtils {
    static isEqual(base, operand) {
        return base.x === operand.x && base.y === operand.y;
    }
    static subtract(base, operand) {
        return { x: base.x - operand.x, y: base.y - operand.y };
    }
    static add(base, operand) {
        return { x: base.x + operand.x, y: base.y + operand.y };
    }
    static multiply(base, operand) {
        return { x: base.x * operand, y: base.y * operand };
    }
    static toString(coords) {
        return `x: ${coords.x}, y: ${coords.y}`;
    }
    static sum(coords) {
        return coords.x + coords.y;
    }
    static zero() {
        return { x: 0, y: 0 };
    }
}

class SizeUtils {
    static isEqual(base, operand) {
        return base.width === operand.width && base.height === operand.height;
    }
    static subtract(base, operand) {
        return {
            width: base.width - operand.width,
            height: base.height - operand.height
        };
    }
    static add(base, operand) {
        return {
            width: base.width + operand.width,
            height: base.height + operand.height
        };
    }
    static multiply(base, operand) {
        return { width: base.width * operand, height: base.height * operand };
    }
    static toString(size) {
        return `width: ${size.width}, height: ${size.height}`;
    }
    static zero() {
        return { width: 0, y: 0 };
    }
}

const generateId = () => {
    return v4();
};
const clamp = (num, min, max) => {
    return Math.max(Math.min(num, max), min);
};
const roundToOneDecimalPlace = (num) => {
    return Math.round(num * 10) / 10;
};
const setMapCursor = (cursor) => {
    const interactiveContainer = document.querySelector('#isoflow_map_container');
    if (interactiveContainer) {
        interactiveContainer.style.cursor = cursor;
    }
};
function getItemByIdOrThrow(values, id) {
    const index = values.findIndex((item) => {
        return String(item.id) === String(id);
    });
    if (index === -1) {
        throw new Error(`Item with id "${id}" not found.`);
    }
    return { value: values[index], index: index };
}

const findPath = ({ gridSize, from, to }) => {
    const grid = new PF.Grid(gridSize.width, gridSize.height);
    const finder = new PF.AStarFinder({
        heuristic: PF.Heuristic.manhattan,
        diagonalMovement: PF.DiagonalMovement.Always,
    });
    const path = finder.findPath(from.x, from.y, to.x, to.y, grid);
    const pathTiles = path.map((tile) => {
        return {
            x: tile[0],
            y: tile[1],
        };
    });
    return pathTiles;
};

const getColorVariant = (color, variant, { alpha = 1, grade = 1 }) => {
    switch (variant) {
        case 'light':
            return chroma(color).brighten(grade).alpha(alpha).css();
        case 'dark':
            return chroma(color).darken(grade).saturate(grade).alpha(alpha).css();
        default:
            return chroma(color).alpha(alpha).css();
    }
};
const toPx = (value) => {
    return `${value}px`;
};

/**
 * TODO: This file could do with better organisation and convention for easier reading.
 */
/*
const TILE_PROJECTION_MULTIPLIERS: Size = {
  width: 1.415,
  height: 0.819,
}
const PROJECTED_TILE_SIZE = {
  width: UNPROJECTED_TILE_SIZE * TILE_PROJECTION_MULTIPLIERS.width,
  height: UNPROJECTED_TILE_SIZE * TILE_PROJECTION_MULTIPLIERS.height,
}
*/
// TODO: why is 100? and for what is need UNPROJECTED tile size?
const UNPROJECTED_TILE_SIZE = 100;
const RECT_STROKE_WIDTH = 1;
// Flat tile with sizes "82x82" is optimal width and height for visual perception
const PROJECTED_TILE_SIZE = {
    width: 82,
    height: 82,
};
const APP_PADDING = {
    x: 40,
    y: 40,
};
const DEFAULT_COLOR = '#a5b8f3';
const DIAGRAM_BACKGROUND = '#f6faff';
const TOOL_MENU_HEIGHT = 40;
const DEFAULT_FONT_FAMILY = 'Roboto, Arial, sans-serif';
const VIEW_DEFAULTS = {
    name: 'Untitled view',
    entitiesList: [],
    connectors: [],
    rectangles: [],
    textBoxes: [],
};
const VIEW_ITEM_DEFAULTS = {
    id: null,
    typeId: null,
    name: '',
    iconUrl: '',
    tile: null,
    isIsometric: true,
    labelHeight: 80,
    description: '',
};
const CONNECTOR_DEFAULTS = {
    width: 10,
    description: '',
    anchors: [],
    style: 'SOLID',
};
// The boundaries of the search area for the pathfinder algorithm
// is the grid that encompasses the two nodes + the offset below.
const CONNECTOR_SEARCH_OFFSET = { x: 1, y: 1 };
const TEXTBOX_DEFAULTS = {
    orientation: 'X',
    fontSize: 0.6,
    content: 'Text',
};
const TEXTBOX_PADDING = 0.2;
const TEXTBOX_FONT_WEIGHT = 'bold';
const RECTANGLE_DEFAULTS = {};
const ZOOM_INCREMENT = 0.1;
const MIN_ZOOM = 0.1;
const MAX_ZOOM = 1;
const TRANSFORM_ANCHOR_WIDTH = 30;
const TRANSFORM_CONTROLS_STROKE_WIDTH = 2;
const TRANSFORM_CONTROLS_STROKE_COLOR = '#0392ff';
const DEFAULT_ENTITY_TYPE_ITEM = {
    id: 'default',
    name: 'block',
    isIsometric: true,
    iconUrl: '',
};
const DEFAULT_LABEL_HEIGHT = 20;
const PROJECT_BOUNDING_BOX_PADDING = 3;
const MARKDOWN_EMPTY_VALUE = '<p><br></p>';

// TODO: WTF CONST IN FILE WITH TYPES!!!!
const ProjectionOrientationEnum = {
    X: 'X',
    Y: 'Y',
};
const EditorModeEnum = {
    NON_INTERACTIVE: 'NON_INTERACTIVE',
    EXPLORABLE_READONLY: 'EXPLORABLE_READONLY',
    EDITABLE: 'EDITABLE',
};

// import { id } from './common'
// export const colorSchema = z.object({
//   id,
//   value: z.string().max(7)
// });
const colorSchema = z.string().max(7);
const colorsSchema = z.array(colorSchema);

const coords = z.object({
    x: z.number(),
    y: z.number(),
});
const id = z.string();
z.string();
const constrainedStrings = {
    name: z.string().max(100),
    description: z.string().max(1000),
};

const modelDefaults = {
    title: 'Untitled',
    version: '1.0.12',
    description: '',
    mapEntityTypesList: [],
    colors: ['#a5b8f3'],
    items: [],
    views: [],
};

// export const iconSchema = z.object({
//   id: id,
//   name: constrainedStrings.name,
//   url: z.string(),
//   group: constrainedStrings.name.optional(),
//   isIsometric: z.boolean().optional(),
// })
const mapEntityTypeSchema = z.object({
    id: id,
    name: constrainedStrings.name,
    iconUrl: z.string(),
    group: constrainedStrings.name.optional(),
    isIsometric: z.boolean().optional(),
});

const modelItemSchema = z.object({
    id,
    name: constrainedStrings.name,
    description: constrainedStrings.description.optional(),
    icon: id.optional()
});
const modelItemsSchema = z.array(modelItemSchema);

// Converts a mouse position to a tile position
const screenToIsoTile = (params) => {
    const { mouse, zoom, scroll, rendererSize } = params;
    const isoTileSize = getIsoProjectedTileSize(PROJECTED_TILE_SIZE);
    const tileSize = SizeUtils.multiply(isoTileSize, zoom);
    const halfWidth = tileSize.width / 2;
    const halfHeight = tileSize.height / 2;
    const projectPosition = {
        x: -rendererSize.width * 0.5 + mouse.x - scroll.position.x,
        y: -rendererSize.height * 0.5 + mouse.y - scroll.position.y,
    };
    const tile = {
        x: Math.floor((projectPosition.x + halfWidth) / tileSize.width - projectPosition.y / tileSize.height),
        y: -Math.floor((projectPosition.y + halfHeight) / tileSize.height + projectPosition.x / tileSize.width),
    };
    return tile;
};
const screenToFlatTile = (params) => {
    const { mouse, zoom, scroll, rendererSize } = params;
    const tileSize = SizeUtils.multiply(PROJECTED_TILE_SIZE, zoom);
    const halfWidth = tileSize.width / 2;
    const halfHeight = tileSize.height / 2;
    const projectPosition = {
        x: -rendererSize.width * 0.5 + mouse.x - scroll.position.x,
        y: -rendererSize.height * 0.5 + mouse.y - scroll.position.y,
    };
    const tile = {
        x: Math.floor((projectPosition.x + halfWidth) / tileSize.width),
        y: -Math.floor((projectPosition.y + halfHeight) / tileSize.height),
    };
    return tile;
};
const getTileIsoPosition = (params) => {
    let { tile, directionOffsetsList = ['CENTER'] } = params;
    directionOffsetsList = Array.isArray(directionOffsetsList)
        ? directionOffsetsList
        : [directionOffsetsList];
    const isoTileSize = getIsoProjectedTileSize(PROJECTED_TILE_SIZE);
    const halfWidth = isoTileSize.width / 2;
    const halfHeight = isoTileSize.height / 2;
    let position = {
        x: halfWidth * tile.x - halfWidth * tile.y,
        // Tile rotated in isometric mode and rect is shifted
        // by half the height along the Y coordinate and
        // by half the width along the X coordinate
        // Thats why we need add half height fot top position("+ halfHeight" on end)
        y: -(halfHeight * tile.x + halfHeight * tile.y + halfHeight),
    };
    directionOffsetsList.forEach((directionOffsetKey) => {
        switch (directionOffsetKey) {
            case 'TOP':
                position = CoordsUtils.add(position, { x: 0, y: -halfHeight });
                break;
            case 'BOTTOM':
                position = CoordsUtils.add(position, { x: 0, y: halfHeight });
                break;
            case 'LEFT':
                position = CoordsUtils.add(position, { x: -halfWidth, y: 0 });
                break;
            case 'RIGHT':
                position = CoordsUtils.add(position, { x: halfWidth, y: 0 });
                break;
        }
    });
    return position;
};
const getTileFlatPosition = (params) => {
    const { tile, directionOffsetsList = ['CENTER'] } = params;
    const halfWidth = PROJECTED_TILE_SIZE.width / 2;
    const halfHeight = PROJECTED_TILE_SIZE.height / 2;
    let position = {
        x: tile.x * PROJECTED_TILE_SIZE.width,
        y: -(tile.y * PROJECTED_TILE_SIZE.height) - halfHeight,
    };
    directionOffsetsList.forEach((directionOffsetKey) => {
        switch (directionOffsetKey) {
            case 'TOP':
                position = CoordsUtils.add(position, { x: 0, y: -halfHeight });
                break;
            case 'BOTTOM':
                position = CoordsUtils.add(position, { x: 0, y: halfHeight });
                break;
            case 'LEFT':
                position = CoordsUtils.add(position, { x: -halfWidth, y: 0 });
                break;
            case 'RIGHT':
                position = CoordsUtils.add(position, { x: halfWidth, y: 0 });
                break;
        }
    });
    return position;
};
const getTileScrollPosition = (tile, origin) => {
    const tilePosition = getTileIsoPosition({ tile: tile, directionOffsetsList: [origin] });
    return {
        x: -tilePosition.x,
        y: -tilePosition.y,
    };
};
const isoToScreen = ({ tile, origin, rendererSize }) => {
    const position = getTileIsoPosition({ tile: tile, directionOffsetsList: [origin] });
    return {
        x: position.x + rendererSize.width / 2,
        y: position.y + rendererSize.height / 2,
    };
};
const getMouse = (params) => {
    const { interactiveElement, zoom, scroll, lastMouse, mouseEvent, rendererSize, isIsometricMode } = params;
    const componentOffset = interactiveElement.getBoundingClientRect();
    const offset = {
        x: componentOffset?.left ?? 0,
        y: componentOffset?.top ?? 0,
    };
    const { clientX, clientY } = mouseEvent;
    const mousePosition = {
        x: clientX - offset.x,
        y: clientY - offset.y,
    };
    const screenParams = {
        mouse: mousePosition,
        zoom: zoom,
        scroll: scroll,
        rendererSize: rendererSize,
    };
    const mouseTile = isIsometricMode ? screenToIsoTile(screenParams) : screenToFlatTile(screenParams);
    const newPosition = {
        screen: mousePosition,
        tile: mouseTile,
    };
    const newDelta = {
        screen: CoordsUtils.subtract(newPosition.screen, lastMouse.position.screen),
        tile: CoordsUtils.subtract(newPosition.tile, lastMouse.position.tile),
    };
    const getMousedown = () => {
        switch (mouseEvent.type) {
            case 'mousedown':
                return newPosition;
            case 'mousemove':
                return lastMouse.mousedown;
            default:
                return null;
        }
    };
    const nextMouse = {
        position: newPosition,
        delta: newDelta,
        mousedown: getMousedown(),
    };
    return nextMouse;
};
const hasMovedTile = (mouse) => {
    if (!mouse.delta) {
        return false;
    }
    return !CoordsUtils.isEqual(mouse.delta.tile, CoordsUtils.zero());
};

const getTextWidth = (text, fontProps) => {
    if (!text) {
        return 0;
    }
    const paddingX = TEXTBOX_PADDING * UNPROJECTED_TILE_SIZE;
    const fontSizePx = toPx(fontProps.fontSize * UNPROJECTED_TILE_SIZE);
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    if (!context) {
        throw new Error('Could not get canvas context');
    }
    context.font = `${fontProps.fontWeight} ${fontSizePx} ${fontProps.fontFamily}`;
    const metrics = context.measureText(text);
    canvas.remove();
    // original:
    // return (metrics.width + paddingX * 2) / UNPROJECTED_TILE_SIZE - 0.8
    const filledTilesCount = (metrics.width + paddingX * 2) / PROJECTED_TILE_SIZE.width;
    return Math.ceil(filledTilesCount);
};
const getTextBoxDimensions = (textBox) => {
    const width = getTextWidth(textBox.content, {
        fontSize: textBox.fontSize ?? TEXTBOX_DEFAULTS.fontSize,
        fontFamily: DEFAULT_FONT_FAMILY,
        fontWeight: TEXTBOX_FONT_WEIGHT,
    });
    const height = 1;
    return { width, height };
};
const getTextBoxEndTile = (textBox, size) => {
    const tilesCount = size.width > 0 ? size.width - 1 : size.width;
    if (textBox.orientation === ProjectionOrientationEnum.X) {
        return CoordsUtils.add(textBox.tile, {
            x: tilesCount,
            y: 0,
        });
    }
    else {
        return CoordsUtils.add(textBox.tile, {
            x: 0,
            y: -tilesCount,
        });
    }
};

const sortByPosition = (tiles) => {
    const sortedByX = tiles.slice();
    const sortedByY = tiles.slice();
    sortedByX.sort((a, b) => a.x - b.x);
    sortedByY.sort((a, b) => a.y - b.y);
    const lowest = { byX: sortedByX[0], byY: sortedByY[0] };
    const highest = {
        byX: sortedByX[sortedByX.length - 1],
        byY: sortedByY[sortedByY.length - 1],
    };
    const lowX = lowest.byX.x;
    const highX = highest.byX.x;
    const lowY = lowest.byY.y;
    const highY = highest.byY.y;
    return {
        byX: sortedByX,
        byY: sortedByY,
        lowest: lowest,
        highest: highest,
        lowX: lowX,
        highX: highX,
        lowY: lowY,
        highY: highY,
    };
};
const isWithinBounds = (tile, bounds) => {
    const { lowX, lowY, highX, highY } = sortByPosition(bounds);
    const isTileInsideCoordsByRight = tile.x >= lowX;
    const isTileInsideCoordsByLeft = tile.x <= highX;
    const isTileInsideCoordsByTop = tile.y >= lowY;
    const isTileInsideCoordsByBottom = tile.y <= highY;
    return (isTileInsideCoordsByRight &&
        isTileInsideCoordsByLeft &&
        isTileInsideCoordsByTop &&
        isTileInsideCoordsByBottom);
};
const isBoundsIntersection = (boundsFirst, boundsSecond) => {
    /**
     * With:
     * firstLowX - left coord for first bounds
     * firstHighX - right coord for first bounds
     * firstLowY - bottom coord for first bounds
     * firstHighY - top coord for first bounds
     */
    const { lowX: firstLowX, lowY: firstLowY, highX: firstHighX, highY: firstHighY, } = sortByPosition(boundsFirst);
    /**
     * With:
     * secondLowX - left coord for second bounds
     * secondHighX - right coord for second bounds
     * secondLowY - bottom coord for second bounds
     * secondHighY - top coord for second bounds
     */
    const { lowX: secondLowX, lowY: secondLowY, highX: secondHighX, highY: secondHighY, } = sortByPosition(boundsSecond);
    const isInterceptByX = firstLowX <= secondHighX && firstHighX >= secondLowX;
    const isInterceptByY = firstLowY <= secondHighY && firstHighY >= secondLowY;
    return isInterceptByX && isInterceptByY;
};
// Returns the four corners of a grid that encapsulates all tiles
// passed in (at least 1 tile needed)
const getBoundingBox = (tiles, offset = CoordsUtils.zero()) => {
    const { lowX, lowY, highX, highY } = sortByPosition(tiles);
    return [
        { x: lowX - offset.x, y: lowY - offset.y },
        { x: highX + offset.x, y: lowY - offset.y },
        { x: highX + offset.x, y: highY + offset.y },
        { x: lowX - offset.x, y: highY + offset.y },
    ];
};
const getBoundingBoxSize = (boundingBox) => {
    const { lowX, lowY, highX, highY } = sortByPosition(boundingBox);
    return {
        width: highX - lowX + 1,
        height: highY - lowY + 1,
    };
};
// Returns a complete set of tiles that form a grid area (takes in any number of tiles to use points to encapsulate)
const getGridSubset = (tiles) => {
    /* Usage:
      const gridSubset = getGridSubset([
        { x: 5, y: 5 },
        { x: 7, y: 7 }
      ]);
  
      gridSubset:
      [
        { x: 5, y: 5 },
        { x: 5, y: 6 },
        { x: 5, y: 7 },
        { x: 6, y: 5 },
        { x: 6, y: 6 },
        { x: 6, y: 7 },
        { x: 7, y: 5 },
        { x: 7, y: 6 },
        { x: 7, y: 7 }
      ]
    */
    const { lowX, lowY, highX, highY } = sortByPosition(tiles);
    const subset = [];
    for (let x = lowX; x < highX + 1; x += 1) {
        for (let y = lowY; y < highY + 1; y += 1) {
            subset.push({ x, y });
        }
    }
    return subset;
};
// TODO: on tile may be many different objects and views
const getFirstItemAtTile = (params) => {
    const { tile, viewItem } = params;
    if (!tile || !viewItem) {
        // Or throw Error?
        return null;
    }
    const entityItem = viewItem.entitiesList.find((item) => {
        return CoordsUtils.isEqual(item.tile, tile);
    });
    if (entityItem) {
        return {
            type: 'ITEM',
            id: entityItem.id,
        };
    }
    const textBox = viewItem.textBoxes.find((tb) => {
        const textBoxTo = getTextBoxEndTile(tb, tb.size);
        const textBoxBounds = getBoundingBox([
            tb.tile,
            {
                x: Math.ceil(textBoxTo.x),
                y: tb.orientation === 'X' ? Math.ceil(textBoxTo.y) : Math.floor(textBoxTo.y),
            },
        ]);
        return isWithinBounds(tile, textBoxBounds);
    });
    if (textBox) {
        return {
            type: 'TEXTBOX',
            id: textBox.id,
        };
    }
    const connector = viewItem.connectors.find((con) => {
        return con.path.tiles.find((pathTile) => {
            const globalPathTile = connectorPathTileToGlobal(pathTile, con.path.rectangle.from);
            return CoordsUtils.isEqual(globalPathTile, tile);
        });
    });
    if (connector) {
        return {
            type: 'CONNECTOR',
            id: connector.id,
        };
    }
    const temporaryReversedRectangles = viewItem.rectangles.slice().reverse();
    const rectangle = temporaryReversedRectangles.find((rectangleItem) => {
        return isWithinBounds(tile, [rectangleItem.from, rectangleItem.to]);
    });
    if (rectangle) {
        return {
            type: 'RECTANGLE',
            id: rectangle.id,
        };
    }
    return null;
};
const getAllEntitiesInBounds = (params) => {
    const { boundsList, viewItem } = params;
    if (!boundsList?.length || !viewItem) {
        // Or throw Error?
        return null;
    }
    const entitiesInBoundsList = viewItem.entitiesList.filter((mapItem) => {
        return isBoundsIntersection(boundsList, [mapItem.tile, mapItem.tile]);
    });
    return entitiesInBoundsList;
};
const getAllTextBoxesInBounds = (params) => {
    const { boundsList, viewItem } = params;
    if (!boundsList?.length || !viewItem) {
        // Or throw Error?
        return null;
    }
    const textBoxesInBoundsList = viewItem.textBoxes.filter((textBoxItem) => {
        const textBoxTo = getTextBoxEndTile(textBoxItem, textBoxItem.size);
        const textBoxBounds = getBoundingBox([
            textBoxItem.tile,
            {
                x: Math.ceil(textBoxTo.x),
                y: textBoxItem.orientation === 'X' ? Math.ceil(textBoxTo.y) : Math.floor(textBoxTo.y),
            },
        ]);
        return isBoundsIntersection(boundsList, textBoxBounds);
    });
    return textBoxesInBoundsList;
};
const getAllConnectorsInBounds = (params) => {
    const { boundsList, viewItem } = params;
    if (!boundsList?.length || !viewItem) {
        // Or throw Error?
        return null;
    }
    // TODO: connectors is disabled in current time
    // const connectorsInBoundsList = viewItem.connectors.filter((connectorItem) => {
    //   const pathOnTile = connectorItem.path.tiles.find((pathTile) => {
    //     const globalPathTile = connectorPathTileToGlobal(pathTile, connectorItem.path.rectangle.from)
    //     return CoordsUtils.isEqual(globalPathTile, tile)
    //   })
    //   return Boolean(pathOnTile)
    // })
    return [];
};
const getAllRectanglesInBounds = (params) => {
    const { boundsList, viewItem } = params;
    if (!boundsList?.length || !viewItem) {
        // Or throw Error?
        return null;
    }
    const temporaryReversedRectangles = viewItem.rectangles.slice().reverse();
    const rectanglesInBoundsList = temporaryReversedRectangles.filter((rectangleItem) => {
        return isBoundsIntersection(boundsList, [rectangleItem.from, rectangleItem.to]);
    });
    return rectanglesInBoundsList;
};
const getAllItemsInBounds = (params) => {
    const entitiesList = getAllEntitiesInBounds(params);
    const textBoxesList = getAllTextBoxesInBounds(params);
    const connectorsList = getAllConnectorsInBounds(params);
    const rectanglesList = getAllRectanglesInBounds(params);
    return { entitiesList, textBoxesList, connectorsList, rectanglesList };
};
const getProjectBounds = (view, padding = PROJECT_BOUNDING_BOX_PADDING) => {
    const itemTilesList = view.entitiesList.map((mapItem) => mapItem.tile);
    const connectorsList = view.connectors ?? [];
    const connectorTilesList = connectorsList.reduce((resultList, connectorItem) => {
        const path = getConnectorPath({ anchors: connectorItem.anchors, view: view });
        resultList.push(path.rectangle.from, path.rectangle.to);
        return resultList;
    }, []);
    const rectanglesList = view.rectangles ?? [];
    const rectangleTilesList = rectanglesList.reduce((resultList, rectangleItem) => {
        resultList.push(rectangleItem.from, rectangleItem.to);
        return resultList;
    }, []);
    const textBoxesList = view.textBoxes ?? [];
    const textBoxTilesList = textBoxesList.reduce((resultList, textBoxItem) => {
        const textBoxSize = getTextBoxDimensions(textBoxItem);
        resultList.push(textBoxItem.tile, CoordsUtils.add(textBoxItem.tile, {
            x: textBoxSize.width,
            y: textBoxSize.height,
        }));
        return resultList;
    }, []);
    let allTilesList = [
        ...itemTilesList,
        ...connectorTilesList,
        ...rectangleTilesList,
        ...textBoxTilesList,
    ];
    if (allTilesList.length === 0) {
        const centerTile = CoordsUtils.zero();
        allTilesList = [centerTile, centerTile, centerTile, centerTile];
    }
    const corners = getBoundingBox(allTilesList, { x: padding, y: padding });
    return corners;
};
const getUnprojectedBounds = (view, isIsometricMode) => {
    const projectBounds = getProjectBounds(view);
    const cornerPositions = projectBounds.map((cornerItem) => {
        const getTilePosition = isIsometricMode ? getTileIsoPosition : getTileFlatPosition;
        return getTilePosition({ tile: cornerItem });
    });
    const size = getBoundingBoxSize(cornerPositions);
    const sortedCorners = sortByPosition(cornerPositions);
    const topLeft = { x: sortedCorners.lowX, y: sortedCorners.lowY };
    return {
        width: size.width,
        height: size.height,
        x: topLeft.x,
        y: topLeft.y,
    };
};

const getAllAnchors = (connectors) => {
    return connectors.reduce((acc, connector) => {
        return [...acc, ...connector.anchors];
    }, []);
};
const getAnchorTile = (anchor, view) => {
    if (anchor.ref.item) {
        const viewItem = getItemByIdOrThrow(view.entitiesList, anchor.ref.item).value;
        return viewItem.tile;
    }
    if (anchor.ref.anchor) {
        const allAnchors = getAllAnchors(view.connectors ?? []);
        const nextAnchor = getItemByIdOrThrow(allAnchors, anchor.ref.anchor).value;
        return getAnchorTile(nextAnchor, view);
    }
    if (anchor.ref.tile) {
        return anchor.ref.tile;
    }
    throw new Error('Could not get anchor tile.');
};
const getConnectorPath = ({ anchors, view, }) => {
    if (anchors.length < 2) {
        throw new Error(`Connector needs at least two anchors (receieved: ${anchors.length})`);
    }
    const anchorPosition = anchors.map((anchor) => {
        return getAnchorTile(anchor, view);
    });
    const searchArea = getBoundingBox(anchorPosition, CONNECTOR_SEARCH_OFFSET);
    const sorted = sortByPosition(searchArea);
    const searchAreaSize = getBoundingBoxSize(searchArea);
    const rectangle = {
        from: { x: sorted.highX, y: sorted.highY },
        to: { x: sorted.lowX, y: sorted.lowY },
    };
    const positionsNormalisedFromSearchArea = anchorPosition.map((position) => {
        return CoordsUtils.subtract(rectangle.from, position);
    });
    const tiles = positionsNormalisedFromSearchArea.reduce((acc, position, i) => {
        if (i === 0) {
            return acc;
        }
        const prev = positionsNormalisedFromSearchArea[i - 1];
        const path = findPath({
            from: prev,
            to: position,
            gridSize: searchAreaSize,
        });
        return [...acc, ...path];
    }, []);
    return { tiles, rectangle };
};
const connectorPathTileToGlobal = (tile, origin) => {
    return CoordsUtils.subtract(CoordsUtils.subtract(origin, CONNECTOR_SEARCH_OFFSET), CoordsUtils.subtract(tile, CONNECTOR_SEARCH_OFFSET));
};
const getConnectorsByViewMapEntity = (mapItemId, connectors) => {
    return connectors.filter((connector) => {
        return connector.anchors.find((anchor) => {
            return anchor.ref.item === mapItemId;
        });
    });
};
const getConnectorDirectionIcon = (connectorTiles) => {
    if (connectorTiles.length < 2)
        return null;
    const iconTile = connectorTiles[connectorTiles.length - 2];
    const lastTile = connectorTiles[connectorTiles.length - 1];
    let rotation;
    if (lastTile.x > iconTile.x) {
        if (lastTile.y > iconTile.y) {
            rotation = 135;
        }
        else if (lastTile.y < iconTile.y) {
            rotation = 45;
        }
        else {
            rotation = 90;
        }
    }
    if (lastTile.x < iconTile.x) {
        if (lastTile.y > iconTile.y) {
            rotation = -135;
        }
        else if (lastTile.y < iconTile.y) {
            rotation = -45;
        }
        else {
            rotation = -90;
        }
    }
    if (lastTile.x === iconTile.x) {
        if (lastTile.y > iconTile.y) {
            rotation = 180;
        }
        else if (lastTile.y < iconTile.y) {
            rotation = 0;
        }
        else {
            rotation = -90;
        }
    }
    return {
        x: iconTile.x * UNPROJECTED_TILE_SIZE + UNPROJECTED_TILE_SIZE / 2,
        y: iconTile.y * UNPROJECTED_TILE_SIZE + UNPROJECTED_TILE_SIZE / 2,
        rotation: rotation,
    };
};
const convertBoundsToNamedAnchors = (boundingBox) => {
    return {
        BOTTOM_LEFT: boundingBox[0],
        BOTTOM_RIGHT: boundingBox[1],
        TOP_RIGHT: boundingBox[2],
        TOP_LEFT: boundingBox[3],
    };
};
const getAnchorAtTile = (tile, anchors) => {
    return anchors.find((anchor) => {
        return Boolean(anchor.ref.tile && CoordsUtils.isEqual(anchor.ref.tile, tile));
    });
};
const getAnchorParent = (anchorId, connectors) => {
    const connector = connectors.find((con) => {
        return con.anchors.find((anchor) => {
            return anchor.id === anchorId;
        });
    });
    if (!connector) {
        throw new Error(`Could not find connector with anchor id ${anchorId}`);
    }
    return connector;
};

const validateConnectorAnchor = (anchor, ctx) => {
    const issues = [];
    if (Object.keys(anchor.ref).length !== 1) {
        issues.push({
            type: 'INVALID_ANCHOR_REF',
            params: {
                anchor: anchor.id,
                view: ctx.view.id,
                connector: ctx.connector.id,
            },
            message: 'Connector includes an anchor that references more than one item.  An anchor can only reference one item.',
        });
    }
    if (anchor.ref.item) {
        try {
            getItemByIdOrThrow(ctx.view.entitiesList, anchor.ref.item);
        }
        catch (e) {
            issues.push({
                type: 'INVALID_ANCHOR_TO_VIEW_ITEM_REF',
                params: {
                    anchor: anchor.id,
                    viewItem: anchor.ref.item,
                    view: ctx.view.id,
                    connector: ctx.connector.id,
                },
                message: 'Connector includes an anchor that references an item that does not exist in this view.',
            });
        }
    }
    if (anchor.ref.anchor) {
        const targetAnchorId = ctx.allAnchors
            .map(({ id }) => {
            return id;
        })
            .includes(anchor.ref.anchor);
        if (!targetAnchorId) {
            issues.push({
                type: 'INVALID_ANCHOR_TO_ANCHOR_REF',
                params: {
                    destAnchor: anchor.id,
                    srcAnchor: anchor.ref.anchor,
                    view: ctx.view.id,
                    connector: ctx.connector.id,
                },
                message: 'Connector includes an anchor that references another connector anchor that does not exist in this view.',
            });
        }
    }
    return issues;
};
const validateConnector = (connector, ctx) => {
    const issues = [];
    // if (connector.color) {
    //   try {
    //     getItemByIdOrThrow(ctx.model.colors, connector.color)
    //   } catch (e) {
    //     issues.push({
    //       type: 'INVALID_CONNECTOR_COLOR_REF',
    //       params: {
    //         connector: connector.id,
    //         view: ctx.view.id,
    //         color: connector.color,
    //       },
    //       message: 'Connector references a color that does not exist in the model.',
    //     })
    //   }
    // }
    if (connector.anchors.length < 2) {
        issues.push({
            type: 'CONNECTOR_TOO_FEW_ANCHORS',
            params: {
                connector: connector.id,
                view: ctx.view.id,
            },
            message: 'Connector must have at least two anchors.  One for the source and one for the target.',
        });
    }
    const { anchors } = connector;
    anchors.forEach((anchor) => {
        const anchorIssues = validateConnectorAnchor(anchor, {
            view: ctx.view,
            connector: connector,
            allAnchors: ctx.allAnchors,
        });
        issues.push(...anchorIssues);
    });
    return issues;
};
const validateRectangle = (rectangle, ctx) => {
    const issues = [];
    if (rectangle.color) {
        try {
            getItemByIdOrThrow(ctx.model.colors, rectangle.color);
        }
        catch (e) {
            issues.push({
                type: 'INVALID_RECTANGLE_COLOR_REF',
                params: {
                    rectangle: rectangle.id,
                    view: ctx.view.id,
                    color: rectangle.color,
                },
                message: 'Rectangle references a color that does not exist in the model.',
            });
        }
    }
    return issues;
};
const validateView = (view, ctx) => {
    const issues = [];
    if (view.connectors) {
        const allAnchors = getAllAnchors(view.connectors);
        view.connectors.forEach((connector) => {
            issues.push(...validateConnector(connector, {
                view,
                model: ctx.model,
                allAnchors,
            }));
        });
    }
    if (view.rectangles) {
        view.rectangles.forEach((rectangle) => {
            issues.push(...validateRectangle(rectangle, {
                view,
                model: ctx.model,
            }));
        });
    }
    view.entitiesList.forEach((viewItem) => {
        try {
            getItemByIdOrThrow(ctx.model.items, viewItem.modelItem);
        }
        catch (e) {
            issues.push({
                type: 'INVALID_VIEW_ITEM_TO_MODEL_ITEM_REF',
                params: {
                    modelItem: viewItem.id,
                    view: view.id,
                },
                message: 'Invalid item in view.  The item references a non-existant item in the model.',
            });
        }
    });
    return issues;
};
const validateModelItem = (modelItem, ctx) => {
    const issues = [];
    if (!modelItem.icon)
        return issues;
    try {
        getItemByIdOrThrow(ctx.model.icons, modelItem.icon);
    }
    catch (e) {
        issues.push({
            type: 'INVALID_MODEL_TO_ICON_REF',
            params: {
                modelItem: modelItem.id,
                icon: modelItem.icon,
            },
            message: 'Invalid item found in the model.  The item references an icon that does not exist.',
        });
    }
    return issues;
};
const validateModel = (model) => {
    const issues = [];
    model.items.forEach((modelItem) => {
        issues.push(...validateModelItem(modelItem, { model }));
    });
    model.views.forEach((view) => {
        issues.push(...validateView(view, { model }));
    });
    return issues;
};

const connectorStyleOptions = ['SOLID', 'DOTTED', 'DASHED'];
const anchorSchema = z.object({
    id: id,
    ref: z
        .object({
        item: id,
        anchor: id,
        tile: coords,
    })
        .partial(),
});
const connectorSchema = z.object({
    id: id,
    description: constrainedStrings.description.optional(),
    color: id.optional(),
    width: z.number().optional(),
    style: z.enum(connectorStyleOptions).optional(),
    anchors: z.array(anchorSchema),
    path: z.object({
        tiles: z.array(coords),
        rectangle: z.object({
            from: coords,
            to: coords,
        }),
    }),
});

const rectangleSchema = z.object({
    id: id,
    color: id.optional(),
    from: coords,
    to: coords,
});

const textBoxOrientationOptions = ['X', 'Y'];
const textBoxSchema = z.object({
    id: id,
    content: constrainedStrings.name,
    fontSize: z.number().optional(),
    orientation: z.enum(textBoxOrientationOptions).optional(),
    size: z.object({
        width: z.number(),
        height: z.number(),
    }),
    tile: coords,
});

const MapEntityItemSchema = z.object({
    id: id,
    typeId: id,
    name: z.string(),
    iconUrl: z.string(),
    tile: coords,
    isIsometric: z.boolean().optional(),
    labelHeight: z.number().optional(),
});
const viewSchema = z.object({
    id: id,
    lastUpdated: z.string().datetime().optional(),
    name: constrainedStrings.name,
    description: constrainedStrings.description.optional(),
    entitiesList: z.array(MapEntityItemSchema),
    rectangles: z.array(rectangleSchema), // .optional()
    connectors: z.array(connectorSchema), // .optional()
    textBoxes: z.array(textBoxSchema), // .optional()
});
const viewsSchema = z.array(viewSchema);

const modelSchema = z
    .object({
    version: z.string().max(10).optional(),
    title: constrainedStrings.name,
    description: constrainedStrings.description.optional(),
    colors: colorsSchema,
    mapEntityTypesList: z.array(mapEntityTypeSchema),
    items: modelItemsSchema,
    views: viewsSchema,
})
    .superRefine((model, ctx) => {
    const issues = validateModel({ ...modelDefaults, ...model });
    issues.forEach((issue) => {
        ctx.addIssue({
            code: z.ZodIssueCode.custom,
            params: issue.params,
            message: issue.message,
        });
    });
});

// TODO: remove last usage on textbox controls
const _OLD_ISO_PROJECTION_BASE_VALUES_LIST = [0.7, -0.405, 0.7, 0.41, 0, 0];
/**
 * NOTE
 * Made rhombus by instruction from
 * https://design.tutsplus.com/tutorials/how-to-create-advanced-isometric-illustrations-using-the-ssr-method--vector-1058
 *
 * Create a rectangle as square.
 * Shear the object 30 degrees(By X).
 * Scale the square 86.062% vertically(by Y). For web we round up this multiplier.
 * Rotate it on -30 degrees.
 */
const ISO_TRANSFORM_PARAMS = {
    rotate: -30,
    skewX: 30,
    skewY: 0,
    scaleX: 1,
    scaleY: 0.865,
};
const ISO_PROJECTION_TRANSFORM = `rotate(${ISO_TRANSFORM_PARAMS.rotate}) skewX(${ISO_TRANSFORM_PARAMS.skewX}) scale(${ISO_TRANSFORM_PARAMS.scaleX} ${ISO_TRANSFORM_PARAMS.scaleY})`;
const getIsoProjectionTransformCss = (transformParams = {}) => {
    const { rotate = ISO_TRANSFORM_PARAMS.rotate, skewX = ISO_TRANSFORM_PARAMS.skewX, skewY = ISO_TRANSFORM_PARAMS.skewY, scaleX = ISO_TRANSFORM_PARAMS.scaleX, scaleY = ISO_TRANSFORM_PARAMS.scaleY, } = transformParams;
    return `rotate(${rotate}deg) skewX(${skewX}deg) skewY(${skewY}deg) scale(${scaleX}, ${scaleY})`;
};
const getIsoMatrix = (orientation) => {
    switch (orientation) {
        case ProjectionOrientationEnum.Y:
            return produce(_OLD_ISO_PROJECTION_BASE_VALUES_LIST, (draft) => {
                draft[1] = -draft[1];
                draft[2] = -draft[2];
            });
        case ProjectionOrientationEnum.X:
        default:
            return _OLD_ISO_PROJECTION_BASE_VALUES_LIST;
    }
};
const getIsoProjectionCss = (orientation) => {
    const matrixTransformValues = getIsoMatrix(orientation);
    return `matrix(${matrixTransformValues.join(', ')})`;
};
const getTranslateCSS = (translate = { x: 0, y: 0 }) => {
    return `translate(${translate.x}px, ${translate.y}px)`;
};
const incrementZoom = (zoom) => {
    const newZoom = clamp(zoom + ZOOM_INCREMENT, MIN_ZOOM, MAX_ZOOM);
    return roundToOneDecimalPlace(newZoom);
};
const decrementZoom = (zoom) => {
    const newZoom = clamp(zoom - ZOOM_INCREMENT, MIN_ZOOM, MAX_ZOOM);
    return roundToOneDecimalPlace(newZoom);
};
const getFitToViewParams = (view, viewportSize, isIsometricMode) => {
    const projectBounds = getProjectBounds(view);
    const sortedCornerPositions = sortByPosition(projectBounds);
    const boundingBoxSize = getBoundingBoxSize(projectBounds);
    const unprojectedBounds = getUnprojectedBounds(view, isIsometricMode);
    const zoom = clamp(Math.min(viewportSize.width / unprojectedBounds.width, viewportSize.height / unprojectedBounds.height), 0, MAX_ZOOM);
    const scrollTarget = {
        x: (sortedCornerPositions.lowX + boundingBoxSize.width / 2) * zoom,
        y: (sortedCornerPositions.lowY + boundingBoxSize.height / 2) * zoom,
    };
    const scroll = getTileScrollPosition(scrollTarget);
    return { zoom, scroll };
};
const getIsoProjectedTileSize = (projectedTileSize) => {
    if (!projectedTileSize?.width || !projectedTileSize?.height) {
        throw Error('"getIsoProjectedTileSize" error: Need to pass "projectedTileSize" as object with width and height in numeric value');
    }
    // Rect base width
    const tileWidth = projectedTileSize.width;
    // Rect base height
    const tileHeight = projectedTileSize.height;
    /** NOTE
     * Formula for finding the long diagonal of a rhombus:
     * D = a * √(2 + 2 * cos(α))
     * (see "isometric/rhombus_scheme.png")
     */
    const rhombusSideSize = tileWidth;
    const acuteAngle = Math.cos(45);
    const longDiagonalSize = rhombusSideSize * Math.sqrt(2 + 2 * acuteAngle);
    // SVG width well be "142" when tile width is "82".
    // Rect element width must be still as PROJECTED_TILE_SIZE.width.
    const rhombusWidth = Math.floor(longDiagonalSize) - RECT_STROKE_WIDTH;
    return {
        width: rhombusWidth,
        height: tileHeight,
    };
};

export { getIsoProjectionCss as $, toPx as A, TRANSFORM_CONTROLS_STROKE_WIDTH as B, CONNECTOR_DEFAULTS as C, DEFAULT_LABEL_HEIGHT as D, EditorModeEnum as E, TRANSFORM_ANCHOR_WIDTH as F, TRANSFORM_CONTROLS_STROKE_COLOR as G, convertBoundsToNamedAnchors as H, ISO_PROJECTION_TRANSFORM as I, getTextBoxEndTile as J, setMapCursor as K, hasMovedTile as L, MARKDOWN_EMPTY_VALUE as M, getFirstItemAtTile as N, generateId as O, PROJECTED_TILE_SIZE as P, getAnchorAtTile as Q, RECTANGLE_DEFAULTS as R, SizeUtils as S, TEXTBOX_DEFAULTS as T, UNPROJECTED_TILE_SIZE as U, getAllItemsInBounds as V, getAnchorParent as W, VIEW_ITEM_DEFAULTS as X, getMouse as Y, DEFAULT_ENTITY_TYPE_ITEM as Z, connectorStyleOptions as _, getAllAnchors as a, ProjectionOrientationEnum as a0, MAX_ZOOM as a1, MIN_ZOOM as a2, modelSchema as a3, APP_PADDING as a4, TOOL_MENU_HEIGHT as a5, DIAGRAM_BACKGROUND as a6, DEFAULT_COLOR as a7, VIEW_DEFAULTS as a8, sortByPosition as a9, isWithinBounds as aa, isBoundsIntersection as ab, getBoundingBoxSize as ac, getGridSubset as ad, getAllEntitiesInBounds as ae, getAllTextBoxesInBounds as af, getAllConnectorsInBounds as ag, getAllRectanglesInBounds as ah, getProjectBounds as ai, screenToIsoTile as aj, getTileScrollPosition as ak, isoToScreen as al, getTextWidth as am, getIsoMatrix as an, getTranslateCSS as ao, getConnectorPath as b, getConnectorsByViewMapEntity as c, getTextBoxDimensions as d, CoordsUtils as e, decrementZoom as f, getItemByIdOrThrow as g, getFitToViewParams as h, incrementZoom as i, getUnprojectedBounds as j, getIsoProjectedTileSize as k, RECT_STROKE_WIDTH as l, modelDefaults as m, getBoundingBox as n, ISO_TRANSFORM_PARAMS as o, getIsoProjectionTransformCss as p, getTileIsoPosition as q, getTileFlatPosition as r, connectorPathTileToGlobal as s, getAnchorTile as t, getConnectorDirectionIcon as u, validateConnector as v, getColorVariant as w, TEXTBOX_PADDING as x, DEFAULT_FONT_FAMILY as y, TEXTBOX_FONT_WEIGHT as z };
