/**
 * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
 *
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */

import { Loci } from '../mol-model/loci';
import { Structure, StructureElement } from '../mol-model/structure';
import { Script } from '../mol-script/script';
import { Material } from '../mol-util/material';
import { shallowEqual } from '../mol-util/object';

export { Substance };

type Substance = { readonly layers: ReadonlyArray<Substance.Layer> }

function Substance(layers: ReadonlyArray<Substance.Layer>): Substance {
    return { layers };
}

namespace Substance {
    export type Layer = { readonly loci: StructureElement.Loci, readonly material: Material, readonly clear: boolean }
    export const Empty: Substance = { layers: [] };

    export function areEqual(sA: Substance, sB: Substance) {
        if (sA.layers.length === 0 && sB.layers.length === 0) return true;
        if (sA.layers.length !== sB.layers.length) return false;
        for (let i = 0, il = sA.layers.length; i < il; ++i) {
            if (sA.layers[i].clear !== sB.layers[i].clear) return false;
            if (!shallowEqual(sA.layers[i].material, sB.layers[i].material)) return false;
            if (!Loci.areEqual(sA.layers[i].loci, sB.layers[i].loci)) return false;
        }
        return true;
    }

    export function isEmpty(overpaint: Substance) {
        return overpaint.layers.length === 0;
    }

    export function remap(substance: Substance, structure: Structure) {
        const layers: Substance.Layer[] = [];
        for (const layer of substance.layers) {
            let { loci, material, clear } = layer;
            loci = StructureElement.Loci.remap(loci, structure);
            if (!StructureElement.Loci.isEmpty(loci)) {
                layers.push({ loci, material, clear });
            }
        }
        return { layers };
    }

    export function merge(substance: Substance): Substance {
        if (isEmpty(substance)) return substance;
        const { structure } = substance.layers[0].loci;
        let clearLoci: StructureElement.Loci | undefined = void 0;
        const map = new Map<Material, StructureElement.Loci>();
        let shadowed = StructureElement.Loci.none(structure);
        for (let i = 0, il = substance.layers.length; i < il; ++i) {
            let { loci, material, clear } = substance.layers[il - i - 1]; // process from end
            loci = StructureElement.Loci.subtract(loci, shadowed);
            shadowed = StructureElement.Loci.union(loci, shadowed);
            if (!StructureElement.Loci.isEmpty(loci)) {
                if (clear) {
                    clearLoci = clearLoci
                        ? StructureElement.Loci.union(loci, clearLoci)
                        : loci;
                } else {
                    if (map.has(material)) {
                        loci = StructureElement.Loci.union(loci, map.get(material)!);
                    }
                    map.set(material, loci);
                }
            }
        }
        const layers: Substance.Layer[] = [];
        if (clearLoci) {
            layers.push({ loci: clearLoci, material: Material(), clear: true });
        }
        map.forEach((loci, material) => {
            layers.push({ loci, material, clear: false });
        });
        return { layers };
    }

    export function filter(substance: Substance, filter: Structure): Substance {
        if (isEmpty(substance)) return substance;
        const { structure } = substance.layers[0].loci;
        const layers: Substance.Layer[] = [];
        for (const layer of substance.layers) {
            let { loci, material, clear } = layer;
            // filter by first map to the `filter` structure and
            // then map back to the original structure of the substance loci
            const filtered = StructureElement.Loci.remap(loci, filter);
            loci = StructureElement.Loci.remap(filtered, structure);
            if (!StructureElement.Loci.isEmpty(loci)) {
                layers.push({ loci, material, clear });
            }
        }
        return { layers };
    }

    export type ScriptLayer = { script: Script, material: Material, clear: boolean }
    export function ofScript(scriptLayers: ScriptLayer[], structure: Structure): Substance {
        const layers: Substance.Layer[] = [];
        for (let i = 0, il = scriptLayers.length; i < il; ++i) {
            const { script, material, clear } = scriptLayers[i];
            const loci = Script.toLoci(script, structure);
            if (!StructureElement.Loci.isEmpty(loci)) {
                layers.push({ loci, material, clear });
            }
        }
        return { layers };
    }

    export type BundleLayer = { bundle: StructureElement.Bundle, material: Material, clear: boolean }
    export function ofBundle(bundleLayers: BundleLayer[], structure: Structure): Substance {
        const layers: Substance.Layer[] = [];
        for (let i = 0, il = bundleLayers.length; i < il; ++i) {
            const { bundle, material, clear } = bundleLayers[i];
            const loci = StructureElement.Bundle.toLoci(bundle, structure.root);
            layers.push({ loci, material, clear });
        }
        return { layers };
    }

    export function toBundle(overpaint: Substance) {
        const layers: BundleLayer[] = [];
        for (let i = 0, il = overpaint.layers.length; i < il; ++i) {
            const { loci, material, clear } = overpaint.layers[i];
            const bundle = StructureElement.Bundle.fromLoci(loci);
            layers.push({ bundle, material, clear });
        }
        return { layers };
    }
}