import Err from '@openaddresses/batch-error';
import type { Static } from '@sinclair/typebox';
import type {
    Feature,
    Polygon,
    FeaturePropertyMission,
    FeaturePropertyMissionChange,
    FeaturePropertyMissionLayer,
} from '../types/feature.js';
import type {
    MartiDest,
    MartiDestAttributes,
    Link,
    LinkAttributes,
    ColorAttributes,
    RadMeasurement,
    RadPhysicalModule,
    RadSpectrum,
    RadIsotope,
    ChemDetection,
    BioMeasurement,
    BioMeasurementLevel,
    BioMeasurementLevelAttributes,
} from '../types/types.js'
import Ellipse from '@turf/ellipse';
import Truncate from '@turf/truncate';
import { destination } from '@turf/destination';
import Color from '../utils/color.js';
import JSONCoT from '../types/types.js'
import CoT from '../cot.js';

// GeoJSON Geospatial ops will truncate to the below
const COORDINATE_PRECISION = 6;
const ELLIPSE_TYPE_PREFIXES = ['u-d-c-c', 'u-r-b-c-c', 'u-d-c-e'];

/**
 * Return a GeoJSON Feature from an XML CoT message
 */
export async function to_geojson(
    cot: CoT
): Promise<Static<typeof Feature>> {
    const raw: Static<typeof JSONCoT> = JSON.parse(JSON.stringify(cot.raw));
    if (!raw.event.detail) raw.event.detail = {};
    if (!raw.event.detail.contact) raw.event.detail.contact = { _attributes: { callsign: 'UNKNOWN' } };
    if (!raw.event.detail.contact._attributes) raw.event.detail.contact._attributes = { callsign: 'UNKNOWN' };

    const feat: Static<typeof Feature> = {
        id: raw.event._attributes.uid,
        type: 'Feature',
        properties: {
            callsign: raw.event.detail.contact._attributes.callsign || 'UNKNOWN',
            center: [ Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae) ],
            type: raw.event._attributes.type,
            how: raw.event._attributes.how || '',
            time: raw.event._attributes.time,
            start: raw.event._attributes.start,
            stale: raw.event._attributes.stale,
        },
        geometry: {
            type: 'Point',
            coordinates: [ Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae) ]
        }
    };

    const contact = JSON.parse(JSON.stringify(raw.event.detail.contact._attributes));
    delete contact.callsign;
    if (Object.keys(contact).length) {
        feat.properties.contact = contact;
    }

    if (cot.creator()) {
        feat.properties.creator = cot.creator();
    }

    if (raw.event.detail.remarks?._text) {
        feat.properties.remarks = raw.event.detail.remarks._text;
    }

    if (raw.event.detail.fileshare) {
        feat.properties.fileshare = raw.event.detail.fileshare._attributes;
        if (typeof feat.properties.fileshare?.sizeInBytes === 'string') {
            feat.properties.fileshare.sizeInBytes = parseInt(feat.properties.fileshare.sizeInBytes)
        }
    }

    if (raw.event.detail.__milsym) {
        feat.properties.milsym = {
            id: raw.event.detail.__milsym._attributes.id
        }
    }

    if (raw.event.detail.__milicon) {
        feat.properties.milicon = {
            id: raw.event.detail.__milicon._attributes.id
        }
    }

    if (raw.event.detail.sensor) {
        feat.properties.sensor = raw.event.detail.sensor._attributes;
    }

    if (raw.event.detail.radsensordetail) {
        const rad = raw.event.detail.radsensordetail;
        feat.properties.radsensordetail = {
            sensor_data: { ...rad.sensor_data._attributes }
        };

        if (rad.radmeasurement) {
            const measurements = Array.isArray(rad.radmeasurement) ? rad.radmeasurement : [rad.radmeasurement];
            feat.properties.radsensordetail.radmeasurement = measurements.map((m: Static<typeof RadMeasurement>) => ({ ...m._attributes }));
        }

        if (rad.physical_module) {
            const modules = Array.isArray(rad.physical_module) ? rad.physical_module : [rad.physical_module];
            feat.properties.radsensordetail.physical_module = modules.map((m: Static<typeof RadPhysicalModule>) => ({ ...m._attributes }));
        }

        if (rad.search_algorithm) {
            feat.properties.radsensordetail.search_algorithm = { ...rad.search_algorithm._attributes };
        }

        if (rad.spectrum) {
            const spectra = Array.isArray(rad.spectrum) ? rad.spectrum : [rad.spectrum];
            feat.properties.radsensordetail.spectrum = spectra.map((s: Static<typeof RadSpectrum>) => ({ ...s._attributes }));
        }

        if (rad.isotope) {
            const isotopes = Array.isArray(rad.isotope) ? rad.isotope : [rad.isotope];
            feat.properties.radsensordetail.isotope = isotopes.map((i: Static<typeof RadIsotope>) => ({ ...i._attributes }));
        }

        if (rad.data_permissions) {
            feat.properties.radsensordetail.data_permissions = { ...rad.data_permissions._attributes };
        }

        if (rad.command_permissions) {
            feat.properties.radsensordetail.command_permissions = { ...rad.command_permissions._attributes };
        }
    }

    if (raw.event.detail.chemsensordetail) {
        const chem = raw.event.detail.chemsensordetail;
        feat.properties.chemsensordetail = {
            sensor_data: { ...chem.sensor_data._attributes }
        };

        if (chem.detection) {
            const detections = Array.isArray(chem.detection) ? chem.detection : [chem.detection];
            feat.properties.chemsensordetail.detection = detections.map((d: Static<typeof ChemDetection>) => ({ ...d._attributes }));
        }
    }

    if (raw.event.detail.biosensordetail) {
        const bio = raw.event.detail.biosensordetail;
        feat.properties.biosensordetail = {
            sensor_data: { ...bio.sensor_data._attributes }
        };

        if (bio.measurement) {
            const measurements = Array.isArray(bio.measurement) ? bio.measurement : [bio.measurement];
            feat.properties.biosensordetail.measurement = measurements.map((m: Static<typeof BioMeasurement>) => {
                const measurement: { level?: Array<Static<typeof BioMeasurementLevelAttributes>> } & typeof m._attributes = { ...m._attributes };
                if (m.level) {
                    const levels = Array.isArray(m.level) ? m.level : [m.level];
                    measurement.level = levels.map((l: Static<typeof BioMeasurementLevel>) => ({ ...l._attributes }));
                }
                return measurement;
            });
        }
    }

    if (raw.event.detail.spatial) {
        const spatial = raw.event.detail.spatial;
        feat.properties.spatial = {
            attitude: { ...spatial.attitude._attributes },
            spin: { ...spatial.spin._attributes }
        };

        if (spatial._attributes?.version !== undefined) {
            feat.properties.spatial.version = spatial._attributes.version;
        }
    }

    if (raw.event.detail.range) {
        feat.properties.range = raw.event.detail.range._attributes.value;
    }

    if (raw.event.detail.bearing) {
        feat.properties.bearing = raw.event.detail.bearing._attributes.value;
    }

    if (raw.event.detail.display?._attributes?.minzoom !== undefined) {
        feat.properties.minzoom = raw.event.detail.display._attributes.minzoom;
    }

    if (raw.event.detail.display?._attributes?.rotate !== undefined) {
        feat.properties.rotate = raw.event.detail.display._attributes.rotate;
    }

    if (raw.event.detail.display?._attributes?.maxzoom !== undefined) {
        feat.properties.maxzoom = raw.event.detail.display._attributes.maxzoom;
    }

    if (raw.event.detail.labels_on?._attributes?.value !== undefined) {
        feat.properties.labels = raw.event.detail.labels_on._attributes.value;
    }

    if (raw.event.detail.__video?._attributes) {
        feat.properties.video = raw.event.detail.__video._attributes;

        if (raw.event.detail.__video.ConnectionEntry) {
            feat.properties.video.connection = raw.event.detail.__video.ConnectionEntry._attributes;
        }
    }

    if (raw.event.detail.__geofence) {
        feat.properties.geofence = raw.event.detail.__geofence._attributes;
    }

    if (raw.event.detail.ackrequest) {
        feat.properties.ackrequest = raw.event.detail.ackrequest._attributes;
    }

    if (raw.event.detail.attachment_list) {
        feat.properties.attachments = JSON.parse(raw.event.detail.attachment_list._attributes.hashes);
    }

    if (raw.event.detail.link) {
        if (!Array.isArray(raw.event.detail.link)) raw.event.detail.link = [raw.event.detail.link];

        // These CoT Types use "point" links as geometry points
        if (['u-d-f', 'u-d-r', 'b-m-r', 'u-rb-a'].includes(raw.event._attributes.type)) {
            feat.properties.links = raw.event.detail.link
                .filter((link: Static<typeof Link>) => {
                    return !link._attributes.point
                }).map((link: Static<typeof Link>): Static<typeof LinkAttributes> => {
                    return link._attributes;
                });
        } else {
            feat.properties.links = raw.event.detail.link
                .map((link: Static<typeof Link>): Static<typeof LinkAttributes> => {
                        return link._attributes;
                });
        }

        if (!feat.properties.links || !feat.properties.links.length) delete feat.properties.links;
    }

    if (raw.event.detail.archive) {
        feat.properties.archived = true;
    }

    if (raw.event.detail.__chat) {
        feat.properties.chat = {
            ...raw.event.detail.__chat._attributes,
            chatgrp: raw.event.detail.__chat.chatgrp
        }
    } else if (raw.event.detail.__chatreceipt) {
        feat.properties.chat = {
            ...raw.event.detail.__chatreceipt._attributes,
            chatgrp: raw.event.detail.__chatreceipt.chatgrp
        }
    }

    if (raw.event.detail.track?._attributes) {
        const track = raw.event.detail.track._attributes;

        const course = Number(track.course);
        if (track.course !== undefined && track.course !== '' && !isNaN(course)) feat.properties.course = course;

        const slope = Number(track.slope);
        if (track.slope !== undefined && track.slope !== '' && !isNaN(slope)) feat.properties.slope = slope;

        const speed = Number(track.speed);
        if (track.speed !== undefined && track.speed !== '' && !isNaN(speed)) feat.properties.speed = speed;
    }

    if (raw.event.detail.marti) {
        if (raw.event.detail.marti._attributes?.archive) {
            feat.properties.marti_archive = true;
        }

        if (raw.event.detail.marti.dest) {
            if (!Array.isArray(raw.event.detail.marti.dest)) raw.event.detail.marti.dest = [raw.event.detail.marti.dest];

            const dest: Array<Static<typeof MartiDestAttributes>> = raw.event.detail.marti.dest.map((d: Static<typeof MartiDest>) => {
                return { ...d._attributes };
            });

            feat.properties.dest = dest.length === 1 ? dest[0] : dest
        }
    }

    if (
        raw.event.detail.usericon?._attributes?.iconsetpath
        && !['b-m-p-s-m'].includes(raw.event._attributes.type)
   ) {
        feat.properties.icon = raw.event.detail.usericon._attributes.iconsetpath;
    }

    if (raw.event.detail.uid?._attributes?.Droid) {
        feat.properties.droid = raw.event.detail.uid._attributes.Droid;
    }

    if (raw.event.detail.takv?._attributes) {
        feat.properties.takv = raw.event.detail.takv._attributes;
    }

    if (raw.event.detail.__group?._attributes) {
        feat.properties.group = raw.event.detail.__group._attributes;
    }

    if (raw.event.detail['_flow-tags_']?._attributes) {
        feat.properties.flow = raw.event.detail['_flow-tags_']._attributes;
    }

    if (raw.event.detail.status?._attributes) {
        feat.properties.status = raw.event.detail.status._attributes;
    }

    if (raw.event.detail.mission?._attributes) {
        const mission: Static<typeof FeaturePropertyMission> = {
            ...raw.event.detail.mission._attributes
        };

        if (raw.event.detail.mission.MissionChanges) {
            const changes =
                Array.isArray(raw.event.detail.mission.MissionChanges)
                    ? raw.event.detail.mission.MissionChanges
                    : [ raw.event.detail.mission.MissionChanges ]

            mission.missionChanges = []
            for (const change of changes) {
                const mc: Static<typeof FeaturePropertyMissionChange> = {
                    contentUid: change.MissionChange.contentUid ? change.MissionChange.contentUid._text : undefined,
                    creatorUid: change.MissionChange.creatorUid._text,
                    isFederatedChange: change.MissionChange.isFederatedChange._text,
                    missionName: change.MissionChange.missionName._text,
                    timestamp: change.MissionChange.timestamp._text,
                    type: change.MissionChange.type._text,
                };

                if (change.MissionChange.contentResource) {
                    const cr = change.MissionChange.contentResource;
                    mc.contentResource = {
                        uid: cr.uid._text,
                        expiration: cr.expiration._text,
                        hash: cr.hash._text,
                        name: cr.name._text,
                        size: parseInt(cr.size._text),
                        submissionTime: cr.submissionTime._text,
                        submitter: cr.submitter._text,
                        filename: cr.filename ? cr.filename._text : undefined,
                        tool: cr.tool ? cr.tool._text : undefined,
                    };
                }

                if (change.MissionChange.details) {
                    mc.details = {
                        ...change.MissionChange.details._attributes,
                        ...change.MissionChange.details.location
                            ? change.MissionChange.details.location._attributes
                            : {}
                    };
                }

                mission.missionChanges.push(mc)
            }
        }


        if (raw.event.detail.mission.missionLayer) {
            const missionLayer: Static<typeof FeaturePropertyMissionLayer> = {};

            if (raw.event.detail.mission.missionLayer.name?._text) {
                missionLayer.name = raw.event.detail.mission.missionLayer.name._text;
            }
            if (raw.event.detail.mission.missionLayer.parentUid?._text) {
                missionLayer.parentUid = raw.event.detail.mission.missionLayer.parentUid._text;
            }
            if (raw.event.detail.mission.missionLayer.type?._text) {
                missionLayer.type = raw.event.detail.mission.missionLayer.type._text;
            }
            if (raw.event.detail.mission.missionLayer.uid?._text) {
                missionLayer.uid = raw.event.detail.mission.missionLayer.uid._text;
            }

            mission.missionLayer = missionLayer;
        }

        feat.properties.mission = mission;
    }

    if (raw.event.detail.precisionlocation?._attributes) {
        feat.properties.precisionlocation = raw.event.detail.precisionlocation._attributes;
    }

    if (raw.event.detail.strokeColor?._attributes?.value !== undefined) {
        const stroke = new Color(Number(raw.event.detail.strokeColor._attributes.value));
        feat.properties.stroke = stroke.as_hex();
        feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
    }

    if (raw.event.detail.strokeWeight?._attributes?.value) {
        feat.properties['stroke-width'] = Number(raw.event.detail.strokeWeight._attributes.value);
    }

    if (raw.event.detail.strokeStyle?._attributes?.value) {
        feat.properties['stroke-style'] = raw.event.detail.strokeStyle._attributes.value;
    }

    if (raw.event.detail.color) {
        let color: Static<typeof ColorAttributes> | null = null;

        if (Array.isArray(raw.event.detail.color) && raw.event.detail.color.length > 1) {
            color = raw.event.detail.color[0];
            if (!color._attributes) color._attributes = {};

            for (let i = raw.event.detail.color.length - 1; i >= 1; i--) {
                if (raw.event.detail.color[i]._attributes) {
                    Object.assign(color._attributes, raw.event.detail.color[i]._attributes);
                }
            }
        } else if (Array.isArray(raw.event.detail.color) && raw.event.detail.color.length === 1) {
            color = raw.event.detail.color[0];
        } else if (!Array.isArray(raw.event.detail.color)) {
            color = raw.event.detail.color;
        }

        if (color?._attributes?.argb) {
            const parsedColor = new Color(Number(color._attributes.argb));
            feat.properties['marker-color'] = parsedColor.as_hex();
            feat.properties['marker-opacity'] = parsedColor.as_opacity() / 255;
        }
    }

    // Line, Polygon style types
    if (['u-d-f', 'u-d-r', 'b-m-r', 'u-rb-a'].includes(raw.event._attributes.type) && Array.isArray(raw.event.detail.link)) {
        const coordinates = [];

        for (const l of raw.event.detail.link) {
            if (!l._attributes.point) continue;
            coordinates.push(
                l._attributes.point.split(',')
                    .map((p: string) => { return Number(p.trim()) })
                    .splice(0, 2)
                    .reverse()
            );
        }

        // Range & Bearing Line
        if (raw.event._attributes.type === 'u-rb-a') {
            const detail = cot.detail();

            if (!detail.range) throw new Error('Range value not provided')
            if (!detail.bearing) throw new Error('Bearing value not provided')

            // TODO Support inclination
            const dest = destination(
                cot.position(),
                detail.range._attributes.value / 1000,
                detail.bearing._attributes.value
            ).geometry.coordinates;

            feat.geometry = {
                type: 'LineString',
                coordinates: [cot.position(), dest]
            };
        } else if (raw.event._attributes.type === 'u-d-r' || (coordinates[0][0] === coordinates[coordinates.length -1][0] && coordinates[0][1] === coordinates[coordinates.length -1][1])) {
            if (raw.event._attributes.type === 'u-d-r') {
                // CoT rectangles are only 4 points - GeoJSON needs to be closed
                coordinates.push(coordinates[0])
            }

            feat.geometry = {
                type: 'Polygon',
                coordinates: [coordinates]
            }

            if (raw.event.detail.fillColor?._attributes?.value !== undefined) {
                const fill = new Color(Number(raw.event.detail.fillColor._attributes.value));
                feat.properties['fill-opacity'] = fill.as_opacity() / 255;
                feat.properties['fill'] = fill.as_hex();
            }
        } else {
            feat.geometry = {
                type: 'LineString',
                coordinates
            }
        }
    } else if (ELLIPSE_TYPE_PREFIXES.some((prefix) => raw.event._attributes.type.startsWith(prefix))) {
        if (!raw.event.detail.shape) throw new Err(400, null, `${raw.event._attributes.type} (Circle) must define shape value`)

        if (
            !raw.event.detail.shape.ellipse
            || !raw.event.detail.shape.ellipse._attributes
        ) throw new Err(400, null, `${raw.event._attributes.type} (Circle) must define ellipse shape value`)

        const ellipse: {
            major: number;
            minor: number;
            angle: number;
            swapAxis?: boolean;
        } = {
            major: Number(raw.event.detail.shape.ellipse._attributes.major),
            minor: Number(raw.event.detail.shape.ellipse._attributes.minor),
            angle: Number(raw.event.detail.shape.ellipse._attributes.angle)
        }

        if (raw.event.detail.shape.ellipse._attributes.swapAxis !== undefined) {
            ellipse.swapAxis = Boolean(raw.event.detail.shape.ellipse._attributes.swapAxis);
        }

        if (
            !Array.isArray(raw.event.detail.shape.link)
            && raw.event.detail.shape.link?._attributes.type === 'b-x-KmlStyle'
            && raw.event.detail.shape.link?.Style
        ) {
            if (raw.event.detail.shape.link.Style.LineStyle?.color) {
                let rawColor = raw.event.detail.shape.link.Style.LineStyle.color._text;
                if (rawColor.startsWith('#')) rawColor = rawColor.substring(1);

                const a = parseInt(rawColor.substring(0, 2), 16);
                const r = parseInt(rawColor.substring(2, 4), 16);
                const g = parseInt(rawColor.substring(4, 6), 16);
                const b = parseInt(rawColor.substring(6, 8), 16);

                const strokeColor = new Color([a, r, g, b]);
                feat.properties.stroke = strokeColor.as_hex();

                feat.properties['stroke-opacity'] = strokeColor.as_opacity() / 255;
            }

            if (raw.event.detail.shape.link.Style.LineStyle?.width) {
                feat.properties['stroke-width'] = Number(raw.event.detail.shape.link.Style.LineStyle.width._text);
            }

            if (raw.event.detail.shape.link.Style.PolyStyle?.color) {
                let rawColor = raw.event.detail.shape.link.Style.PolyStyle.color._text;
                if (rawColor.startsWith('#')) rawColor = rawColor.substring(1);

                const a = parseInt(rawColor.substring(0, 2), 16);
                const r = parseInt(rawColor.substring(2, 4), 16);
                const g = parseInt(rawColor.substring(4, 6), 16);
                const b = parseInt(rawColor.substring(6, 8), 16);

                const fillColor = new Color([a, r, g, b]);
                feat.properties['fill-opacity'] = fillColor.as_opacity() / 255;
                feat.properties['fill'] = fillColor.as_hex();
            }
        }

        feat.geometry = Truncate(Ellipse(
            feat.geometry.coordinates as number[],
            Number(ellipse.major) / 1000,
            Number(ellipse.minor) / 1000,
            {
                angle: 90 - ellipse.angle
            }
        ), {
            precision: COORDINATE_PRECISION,
            coordinates: 3,
            mutate: true
        }).geometry as Static<typeof Polygon>;

        feat.properties.shape = {};
        feat.properties.shape.ellipse = ellipse;
    } else if (raw.event._attributes.type.startsWith('b-m-p-s-p-i')) {
        // TODO: Currently the "shape" tag is only parsed here - asking ARA for clarification if it is a general use tag
        if (raw.event.detail.shape?.polyline?.vertex) {
            const coordinates = [];

            const vertices = Array.isArray(raw.event.detail.shape.polyline.vertex) ? raw.event.detail.shape.polyline.vertex : [raw.event.detail.shape.polyline.vertex];
            for (const v of vertices) {
                coordinates.push([Number(v._attributes.lon), Number(v._attributes.lat)]);
            }

            if (coordinates.length === 1) {
                feat.geometry = { type: 'Point', coordinates: coordinates[0] }
            } else if (raw.event.detail.shape.polyline._attributes?.closed === true) {
                coordinates.push(coordinates[0]);
                feat.geometry = { type: 'Polygon', coordinates: [coordinates] }
            } else {
                feat.geometry = { type: 'LineString', coordinates }
            }
        }

        if (raw.event.detail.shape?.polyline?._attributes) {
            if (raw.event.detail.shape.polyline._attributes.fillColor) {
                const fill = new Color(Number(raw.event.detail.shape.polyline._attributes.fillColor));
                feat.properties['fill-opacity'] = fill.as_opacity() / 255;
                feat.properties['fill'] = fill.as_hex();
            }

            if (raw.event.detail.shape.polyline._attributes.color) {
                const stroke = new Color(Number(raw.event.detail.shape.polyline._attributes.color));
                feat.properties.stroke = stroke.as_hex();
                feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
            }
        }
    } else if (raw.event._attributes.type === 'b-m-p-s-m') {
        if (
            raw.event.detail.usericon?._attributes?.iconsetpath
            && raw.event.detail.usericon._attributes.iconsetpath.startsWith('COT_MAPPING_SPOTMAP/b-m-p-s-m/')
       ) {
            const spot = new Color(Number(raw.event.detail.usericon._attributes.iconsetpath.split('/')[2]));
            feat.properties['marker-color'] = spot.as_hex();
            feat.properties['marker-opacity'] = spot.as_opacity() / 255;
            delete feat.properties.icon;
        }
    }

    feat.properties.metadata = cot.metadata;
    feat.path = cot.path;

    return feat;
}
