import { MarkupSheet2D } from './MarkupSheet2D';
import { BimApi } from '../../BimApi';
import { MarkupEntityBase } from './MarkupEntityBase';
import { LayerApi } from '../layerapi';
import { BimChangeBlob, BimChangeDwg } from '../../loader/bim-api-client';
import { LayerFailure } from '../Layer';
import { isFailure } from '../../fail';
import { Axis, Color4, Matrix, Observable, Vector3, Vector4 } from '../../loader/babylonjs-import';
import { MarkupSheet2DCollection } from './MarkupSheet2DCollection';
import { MarkupArea } from './MarkupArea';
import {
    AreaDto,
    ArrowDto,
    CircleDto,
    EntityBaseDto,
    LineDto,
    MarkupDto,
    MarkupEntityDto,
    MarkupEntityTypes,
    MarkupLayerEvent2D
} from '../MarkupEntityTypes';
import { Vertex3 } from '../../math';
import { MarkupEntity } from './MarkupEntity';
import { MarkupText } from './MarkupText';
import { MarkupLine } from './MarkupLine';
import { MarkupArrow } from './MarkupArrow';
import { MarkupCircle } from './MarkupCircle';

type LayerApi2dDwg = LayerApi<MarkupEntityBase, MarkupDto>;
type SheetsAndLayerApi = [MarkupSheet2DCollection, LayerApi2dDwg];
export type DtoVersion = { major: number; minor: number; patch: number };
/**
 * @alpha NOTE: This API is experimental, expect breaking changes in the future.
 */
export class MarkupSheets2DApi {
    static readonly markupSheetsLayer = 'markup_sheets';

    private readonly _layerApi = new Map<BimChangeDwg | BimChangeBlob, SheetsAndLayerApi>();
    private readonly _version: DtoVersion = { major: 1, minor: 0, patch: 2 };

    constructor(private readonly _api: BimApi) {}

    /**
     * Gets the current dto (data transfer object) version.
     * This version represents the version of the layer API, this version will be assigned to every sheet collection that is saved via this api.
     * If your local version is lower than the one saved to the layer API you will not be able to load the markups from that sheet collection.
     */
    public get version(): DtoVersion {
        return this._version;
    }

    /**
     * Load sheets from LayersApi.
     * @param change Change to load the sheets from.
     * @param observableFactory Function that should return the observable that will be notified when a layer event happens.
     * @returns Either the {@link Layer} or a {@link LayerFailure}. Use {@link isFailure} to check if the return value is a {@link LayerFailure} or a {@link Layer}.
     */
    public async getOrLoadSheets(
        change: BimChangeDwg | BimChangeBlob,
        observableFactory: () => Observable<MarkupLayerEvent2D>
    ): Promise<MarkupSheet2DCollection | LayerFailure> {
        const [sheets, layerApi] = this.getLayerApi(change, observableFactory);
        const layer = await layerApi.getOrLoadLayer(change);
        if (isFailure(layer)) {
            return layer;
        }

        //hack, assigns layer to avoid crashing when layer is empty.
        sheets._layer = sheets._layer ?? layer;

        return sheets;
    }

    /**
     * Unload sheets layer.
     * @param change The change to unload sheets from.
     * @returns True if the sheets layer was unloaded, otherwise false.
     */
    public unloadSheets(change: BimChangeDwg | BimChangeBlob): boolean {
        const [, layerApi] = this._layerApi.get(change) ?? [undefined, undefined];
        this._layerApi.delete(change);
        return layerApi?.unload(change) ?? false;
    }

    /**
     * @hidden
     */
    private getLayerApi(
        change: BimChangeDwg | BimChangeBlob,
        observableFactory: () => Observable<MarkupLayerEvent2D>
    ): SheetsAndLayerApi {
        let entry = this._layerApi.get(change);
        if (!entry) {
            const sheets = new MarkupSheet2DCollection(observableFactory(), change);
            const layerApi = new LayerApi<MarkupEntityBase, MarkupDto>(this._api, MarkupSheets2DApi.markupSheetsLayer, {
                from: (dto, layer) => {
                    //Dont let an old version load a newer version, unless the newer version has no sheets saved
                    //in which case it does not matter if the older version can load and save the sheet at all.
                    if (
                        (dto.version.major > this._version.major ||
                            (dto.version.major === this._version.major && dto.version.minor > this._version.minor) ||
                            (dto.version.major === this._version.major &&
                                dto.version.minor === this._version.minor &&
                                dto.version.patch > this._version.patch)) &&
                        Object.keys(dto.sheets).length > 0
                    ) {
                        throw new Error(
                            `Could not load markups because the sheets in this change was saved with a newer version of the twinfinity API than the current local version.`
                        );
                    }
                    sheets._layer = layer;

                    const createdEntities: MarkupEntityBase[] = [];
                    const createdSheets: MarkupEntityBase[] = [];

                    const tmpVec = Vector3.Zero(); //temporary vector used for transforming and deep copying points

                    const vertexDeepCopyAndConvert = (originalVertex: Vertex3): Vertex3 => {
                        tmpVec.x = originalVertex.x;
                        tmpVec.y = originalVertex.y;
                        tmpVec.z = originalVertex.z;

                        if (dto.version.major === 1 && dto.version.minor === 0 && dto.version.patch === 0) {
                            this.version101CoordinateConversionToRef(tmpVec);
                        }
                        return {
                            x: tmpVec.x,
                            y: tmpVec.y,
                            z: tmpVec.z
                        };
                    };

                    for (const sheetDto of Object.values(dto.sheets)) {
                        const sheet = new MarkupSheet2D(
                            sheetDto.id,
                            sheets,
                            new Map(Object.entries(sheetDto.properties))
                        );

                        createdSheets.push(sheet);

                        for (const entity of Object.values(sheetDto.entities)) {
                            if (entity.type === MarkupEntityTypes.Area) {
                                //we cant use MarkupSheet2D. here since it would assign a new unique id.
                                const deepCopyPoints: Vertex3[] = [];

                                for (let i = 0; i < entity.points.length; i++) {
                                    const originalVertex = entity.points[i];

                                    deepCopyPoints.push(vertexDeepCopyAndConvert(originalVertex));
                                }
                                const area = new MarkupArea({
                                    properties: new Map(Object.entries(entity.properties)),
                                    id: entity.id,
                                    points: deepCopyPoints,
                                    sheet: sheet,
                                    style: {
                                        backgroundColor: new Color4(
                                            entity.color?.r ?? 0,
                                            entity.color?.g ?? 0,
                                            entity.color?.b ?? 1,
                                            entity.color?.a ?? 1
                                        )
                                    }
                                });
                                createdEntities.push(area);
                            } else if (entity.type === MarkupEntityTypes.Line) {
                                const deepCopyPoints: Vertex3[] = [];

                                for (let i = 0; i < entity.linePoints.length; i++) {
                                    const originalVertex = entity.linePoints[i];

                                    deepCopyPoints.push(vertexDeepCopyAndConvert(originalVertex));
                                }

                                const line = new MarkupLine({
                                    properties: new Map(Object.entries(entity.properties)),
                                    id: entity.id,
                                    linePoints: deepCopyPoints,
                                    sheet: sheet,
                                    style: {
                                        backgroundColor: new Color4(
                                            entity.color?.r ?? 0,
                                            entity.color?.g ?? 1,
                                            entity.color?.b ?? 0,
                                            entity.color?.a ?? 1
                                        )
                                    },
                                    thickness: entity.thickness
                                });
                                createdEntities.push(line);
                            } else if (entity.type === MarkupEntityTypes.Arrow) {
                                //conversion to move head point of arrow to the tip of the arrow instead of inbetween the head and body.
                                if (dto.version.major === 1 && dto.version.minor === 0 && dto.version.patch < 2) {
                                    this.version102ArrowConversion(entity);
                                }

                                const arrowPoint = vertexDeepCopyAndConvert(entity.arrowPoint);
                                const bluntPoint = vertexDeepCopyAndConvert(entity.bluntPoint);

                                const line = new MarkupArrow({
                                    properties: new Map(Object.entries(entity.properties)),
                                    id: entity.id,
                                    arrowPoint: arrowPoint,
                                    bluntPoint: bluntPoint,
                                    arrowHeight: entity.arrowHeight,
                                    arrowWidth: entity.arrowWidth,
                                    lineThickness: entity.lineThickness,
                                    sheet: sheet,
                                    style: {
                                        backgroundColor: new Color4(
                                            entity.color?.r ?? 0,
                                            entity.color?.g ?? 1,
                                            entity.color?.b ?? 0,
                                            entity.color?.a ?? 1
                                        )
                                    }
                                });
                                createdEntities.push(line);
                            } else if (entity.type === MarkupEntityTypes.Text) {
                                const deepCopyPosition = Vector3.Zero();
                                deepCopyPosition.x = entity.position.x;
                                deepCopyPosition.y = entity.position.y;
                                deepCopyPosition.z = entity.position.z;

                                const deepCopyNormal = Vector3.Zero();
                                if (entity.normal) {
                                    deepCopyNormal.set(entity.normal.x, entity.normal.y, entity.normal.z);
                                } else {
                                    deepCopyNormal.copyFrom(Axis.Y);
                                }

                                let deepCopyScale: Vector3 | number;
                                if (typeof entity.scale === 'number') {
                                    deepCopyScale = entity.scale;
                                } else {
                                    deepCopyScale = new Vector3(entity.scale.x, entity.scale.y, entity.scale.z);
                                }

                                const text = new MarkupText({
                                    properties: new Map(Object.entries(entity.properties)),
                                    id: entity.id,
                                    text: entity.text,
                                    position: deepCopyPosition,
                                    normal: deepCopyNormal,
                                    scale: deepCopyScale,
                                    sheet: sheet,
                                    style: {
                                        backgroundColor: new Color4(
                                            entity.backgroundColor?.r ?? 1,
                                            entity.backgroundColor?.g ?? 1,
                                            entity.backgroundColor?.b ?? 1,
                                            entity.backgroundColor?.a ?? 1
                                        )
                                    },
                                    rotation: entity.rotation
                                });
                                createdEntities.push(text);
                            } else if (entity.type === MarkupEntityTypes.Circle) {
                                const circlePoint = vertexDeepCopyAndConvert(entity.circlePoint);

                                const circle = new MarkupCircle({
                                    properties: new Map(Object.entries(entity.properties)),
                                    id: entity.id,
                                    circlePoint: circlePoint,
                                    radiusPoint: entity.radiusPoint ?? { x: 0, y: 0, z: 0 },
                                    lineThickness: entity.lineThickness,
                                    sheet: sheet,
                                    style: {
                                        backgroundColor: new Color4(
                                            entity.color?.r ?? 0,
                                            entity.color?.g ?? 1,
                                            entity.color?.b ?? 0,
                                            entity.color?.a ?? 1
                                        )
                                    }
                                });
                                createdEntities.push(circle);
                            }
                        }
                    }

                    //Sheets have to be first in the ret array
                    //TODO: Better comment
                    createdSheets.push(...createdEntities);
                    return createdSheets;
                },
                //TODO: Change from and to functions based on version property on the loaded markup dto
                to: (tOs, layer) => {
                    const dto: MarkupDto = { version: this._version, sheets: {} };

                    const sheets = tOs.filter((f) => f instanceof MarkupSheet2D) as MarkupSheet2D[];
                    const sortOptions: Intl.CollatorOptions = { numeric: true, sensitivity: 'base' };
                    const sortedSheets = sheets.sort((sheet1, sheet2) =>
                        sheet1.id.localeCompare(sheet2.id, undefined, sortOptions)
                    );

                    for (const sheet of sortedSheets) {
                        const ents: { [id: string]: MarkupEntityDto } = {};
                        const entities = [...sheet].sort((entity1, entity2) =>
                            entity1.id.localeCompare(entity2.id, undefined, sortOptions)
                        );

                        for (const entity of entities) {
                            const sortedProperties = [...entity.properties].sort((property1, property2) =>
                                property1[0].localeCompare(property2[0], undefined, sortOptions)
                            );
                            if (entity instanceof MarkupEntity) {
                                const baseObj: EntityBaseDto = {
                                    id: entity.id,
                                    properties: Object.fromEntries(sortedProperties)
                                };

                                if (entity instanceof MarkupArea) {
                                    const points = entity.points; //TODO: Copy instead of reference?

                                    const areaData: Omit<AreaDto, 'id' | 'properties'> = {
                                        points: points,
                                        type: MarkupEntityTypes.Area,
                                        color: {
                                            r: entity.style.backgroundColor.r,
                                            g: entity.style.backgroundColor.g,
                                            b: entity.style.backgroundColor.b,
                                            a: entity.style.backgroundColor.a
                                        }
                                    };
                                    ents[entity.id] = { ...areaData, ...baseObj };
                                } else if (entity instanceof MarkupText) {
                                    ents[entity.id] = {
                                        id: entity.id,
                                        type: MarkupEntityTypes.Text,
                                        properties: Object.fromEntries(sortedProperties),
                                        text: entity.text,
                                        position: {
                                            x: entity.position.x,
                                            y: entity.position.y,
                                            z: entity.position.z
                                        },
                                        normal: {
                                            x: entity.normal.x,
                                            y: entity.normal.y,
                                            z: entity.normal.z
                                        },
                                        scale:
                                            entity.scale instanceof Vector3
                                                ? {
                                                      x: entity.scale.x,
                                                      y: entity.scale.y,
                                                      z: entity.scale.z
                                                  }
                                                : entity.scale,
                                        rotation: entity.rotation,
                                        backgroundColor: {
                                            r: entity.style.backgroundColor.r,
                                            g: entity.style.backgroundColor.g,
                                            b: entity.style.backgroundColor.b,
                                            a: entity.style.backgroundColor.a
                                        }
                                    };
                                } else if (entity instanceof MarkupLine) {
                                    const linePoints = entity.linePoints; //TODO: Copy instead of reference?

                                    const lineData: Omit<LineDto, 'id' | 'properties'> = {
                                        linePoints: linePoints,
                                        type: MarkupEntityTypes.Line,
                                        thickness: entity.thickness,
                                        color: {
                                            r: entity.style.backgroundColor.r,
                                            g: entity.style.backgroundColor.g,
                                            b: entity.style.backgroundColor.b,
                                            a: entity.style.backgroundColor.a
                                        }
                                    };
                                    ents[entity.id] = { ...lineData, ...baseObj };
                                } else if (entity instanceof MarkupArrow) {
                                    const arrowPoint = entity.arrowPoint;
                                    const bluntPoint = entity.bluntPoint;

                                    const arrowData: Omit<ArrowDto, 'id' | 'properties'> = {
                                        bluntPoint: bluntPoint,
                                        arrowPoint: arrowPoint,
                                        type: MarkupEntityTypes.Arrow,
                                        lineThickness: entity.lineThickness,
                                        arrowWidth: entity.arrowWidth,
                                        arrowHeight: entity.arrowHeight,
                                        color: {
                                            r: entity.style.backgroundColor.r,
                                            g: entity.style.backgroundColor.g,
                                            b: entity.style.backgroundColor.b,
                                            a: entity.style.backgroundColor.a
                                        }
                                    };
                                    ents[entity.id] = { ...arrowData, ...baseObj };
                                } else if (entity instanceof MarkupCircle) {
                                    const circlePoint = entity.circlePoint;
                                    const radiusPoint = entity.radiusPoint;
                                    const lineThickness = entity.lineThickness;

                                    const circleData: Omit<CircleDto, 'id' | 'properties'> = {
                                        circlePoint: circlePoint,
                                        radiusPoint: radiusPoint,
                                        lineThickness: lineThickness,
                                        type: MarkupEntityTypes.Circle,
                                        color: {
                                            r: entity.style.backgroundColor.r,
                                            g: entity.style.backgroundColor.g,
                                            b: entity.style.backgroundColor.b,
                                            a: entity.style.backgroundColor.a
                                        }
                                    };
                                    ents[entity.id] = { ...circleData, ...baseObj };
                                }
                            }
                        }

                        dto.sheets[sheet.id] = {
                            id: sheet.id,
                            properties: Object.fromEntries(sheet.properties),
                            type: MarkupEntityTypes.Sheet,
                            entities: ents
                        };
                    }

                    return dto;
                }
            });
            entry = [sheets, layerApi];
            this._layerApi.set(change, entry);
        }
        return entry;
    }

    private version101CoordinateConversionToRef(tmpVec: Vector3): void {
        //since we know during the entire duration of version 1.0.0 the svg/pdf planes default values were
        //position = (0, 0, 0)
        //scale = (50, 1, 50)
        //we can safely transform these coordinates down to local space using a matrix created from these values.
        //by doing this we can then change the scale or position of the plane in a later version and be able to transform
        //the coordinates correctly using a different matrix.
        const tmpMatrix = Matrix.Identity(); //temporary matrix used for transforming points
        tmpMatrix.setRow(0, new Vector4(50, 0, 0, 0));
        tmpMatrix.setRow(1, new Vector4(0, 1, 0, 0));
        tmpMatrix.setRow(2, new Vector4(0, 0, 50, 0));
        tmpMatrix.setRow(3, new Vector4(0, 0, 0, 1));
        tmpMatrix.invert();
        Vector3.TransformCoordinatesToRef(tmpVec, tmpMatrix, tmpVec);
    }

    private version102ArrowConversion(arrow: ArrowDto): void {
        //changes the arrow front point to be at the tip of the arrow rather than where the arrow head starts.
        const arrowPointVec3 = new Vector3(arrow.arrowPoint.x, arrow.arrowPoint.y, arrow.arrowPoint.z);
        const bluntPointVec3 = new Vector3(arrow.bluntPoint.x, arrow.bluntPoint.y, arrow.bluntPoint.z);
        const lineDirection = arrowPointVec3.subtract(bluntPointVec3);
        lineDirection.normalizeToRef(lineDirection);
        const newArrowPoint = arrowPointVec3.add(lineDirection.scale(arrow.arrowHeight));
        arrow.arrowPoint.x = newArrowPoint.x;
        arrow.arrowPoint.y = newArrowPoint.y;
        arrow.arrowPoint.z = newArrowPoint.z;
    }
}
