/*
 * citygml-tools - Collection of tools for processing CityGML files
 * https://github.com/citygml4j/citygml-tools
 *
 * citygml-tools is part of the citygml4j project
 *
 * Copyright 2018-2023 Claus Nagel <claus.nagel@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.citygml4j.tools.util;

import org.citygml4j.core.model.appearance.*;
import org.citygml4j.core.model.core.AbstractAppearanceProperty;
import org.citygml4j.core.model.core.AbstractCityObject;
import org.citygml4j.core.visitor.ObjectWalker;
import org.xmlobjects.gml.model.GMLObject;
import org.xmlobjects.gml.model.base.AbstractAssociation;
import org.xmlobjects.gml.model.base.AbstractGML;
import org.xmlobjects.gml.model.base.AbstractReference;
import org.xmlobjects.gml.model.geometry.AbstractGeometry;
import org.xmlobjects.gml.model.geometry.aggregates.MultiSurface;
import org.xmlobjects.gml.model.geometry.primitives.AbstractSurface;
import org.xmlobjects.gml.util.id.DefaultIdCreator;
import org.xmlobjects.util.copy.CopyBuilder;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class GeometryCopyBuilder {
    private final AppearanceProcessor appearanceProcessor = new AppearanceProcessor();
    private final CopyBuilder copyBuilder = new CopyBuilder().failOnError(true);
    private final String ID = "id";

    private AppearanceHelper globalAppearanceHelper;
    private AppearanceHelper localAppearanceHelper;
    private boolean copyAppearance;

    private GeometryCopyBuilder() {
    }

    public static GeometryCopyBuilder newInstance() {
        return new GeometryCopyBuilder();
    }

    public GeometryCopyBuilder withGlobalAppearanceHelper(AppearanceHelper globalAppearanceHelper) {
        this.globalAppearanceHelper = globalAppearanceHelper;
        return this;
    }

    public GeometryCopyBuilder withLocalAppearanceHelper(AppearanceHelper localAppearanceHelper) {
        this.localAppearanceHelper = localAppearanceHelper;
        return this;
    }

    public GeometryCopyBuilder copyAppearance(boolean copyAppearance) {
        this.copyAppearance = copyAppearance;
        return this;
    }

    public AbstractGeometry copy(AbstractGeometry geometry, GMLObject target) {
        return copy(geometry, getTopLevelObject(target));
    }

    public AbstractGeometry copy(AbstractGeometry geometry) {
        return copy(geometry, null);
    }

    private AbstractGeometry copy(AbstractGeometry geometry, AbstractCityObject topLevelObject) {
        AbstractGeometry copy = copyBuilder.deepCopy(geometry);

        IdHelper idHelper = new IdHelper();
        idHelper.updateIds(copy, copyAppearance);

        if (copyAppearance && topLevelObject != null) {
            appearanceProcessor.process(copy, topLevelObject, idHelper);
        }

        return copy;
    }

    private AbstractCityObject getTopLevelObject(GMLObject object) {
        GMLObject topLevelObject = object;
        GMLObject parent = object;
        while ((parent = parent.getParent(AbstractCityObject.class)) != null) {
            topLevelObject = parent;
        }

        return topLevelObject instanceof AbstractCityObject ?
                (AbstractCityObject) topLevelObject :
                null;
    }

    private class AppearanceProcessor extends ObjectWalker {
        private AbstractCityObject topLevelObject;
        private IdHelper idHelper;
        private int id;

        public void process(AbstractGeometry geometry, AbstractCityObject topLevelObject, IdHelper idHelper) {
            this.topLevelObject = topLevelObject;
            this.idHelper = idHelper;
            geometry.accept(this);
        }

        @Override
        public void visit(AbstractSurface surface) {
            process(surface);
            super.visit(surface);
        }

        @Override
        public void visit(MultiSurface multiSurface) {
            process(multiSurface);
            super.visit(multiSurface);
        }

        private void process(AbstractGeometry geometry) {
            if (localAppearanceHelper != null && localAppearanceHelper.hasAppearances()) {
                process(geometry, localAppearanceHelper);
            }

            if (globalAppearanceHelper != null && globalAppearanceHelper.hasAppearances()) {
                process(geometry, globalAppearanceHelper);
            }
        }

        private void process(AbstractGeometry geometry, AppearanceHelper helper) {
            String geometryId = geometry.getLocalProperties().get(ID, String.class);

            List<TextureAssociationProperty> properties = helper.getParameterizedTextures(geometryId);
            if (properties != null) {
                for (TextureAssociationProperty property : properties) {
                    ParameterizedTexture texture = getOrCreateSurfaceData(property, ParameterizedTexture.class);
                    TextureAssociationProperty copy = copyBuilder.deepCopy(property);
                    idHelper.visit(copy);
                    texture.getTextureParameterizations().add(copy);
                }
            }

            List<GeometryReference> references = helper.getGeoreferencedTextures(geometryId);
            if (references != null) {
                for (GeometryReference reference : references) {
                    GeoreferencedTexture texture = getOrCreateSurfaceData(reference, GeoreferencedTexture.class);
                    texture.getTargets().add(new GeometryReference("#" + idHelper.getId(reference.getHref())));
                }
            }

            references = helper.getMaterials(geometryId);
            if (references != null) {
                for (GeometryReference reference : references) {
                    X3DMaterial material = getOrCreateSurfaceData(reference, X3DMaterial.class);
                    material.getTargets().add(new GeometryReference("#" + idHelper.getId(reference.getHref())));
                }
            }
        }

        private <T extends AbstractSurfaceData> T getOrCreateSurfaceData(AbstractAssociation<?> association, Class<T> type) {
            T source = association.getParent(type);

            Appearance appearance = getOrCreateAppearance(source.getParent(Appearance.class));
            for (AbstractSurfaceDataProperty property : appearance.getSurfaceData()) {
                if (type.isInstance(property.getObject())) {
                    T surfaceData = type.cast(property.getObject());
                    if (surfaceData != source
                            && surfaceData.hasLocalProperties()
                            && source.hasLocalProperties()
                            && source.getLocalProperties().getAndCompare(ID, surfaceData.getLocalProperties().get(ID))) {
                        return surfaceData;
                    }
                }
            }

            T surfaceData = copyBuilder.shallowCopy(source);
            surfaceData.setId(null);
            surfaceData.getLocalProperties().set(ID, source.getLocalProperties().getOrSet(ID, Integer.class, () -> id++));
            appearance.getSurfaceData().add(new AbstractSurfaceDataProperty(surfaceData));

            if (surfaceData instanceof ParameterizedTexture) {
                ((ParameterizedTexture) surfaceData).setTextureParameterizations(null);
            } else if (surfaceData instanceof X3DMaterial) {
                ((X3DMaterial) surfaceData).setTargets(null);
            } else if (surfaceData instanceof GeoreferencedTexture) {
                ((GeoreferencedTexture) surfaceData).setTargets(null);
            }

            return surfaceData;
        }

        private Appearance getOrCreateAppearance(Appearance source) {
            for (AbstractAppearanceProperty property : topLevelObject.getAppearances()) {
                if (property.getObject() instanceof Appearance) {
                    Appearance appearance = (Appearance) property.getObject();
                    if (appearance != source
                            && appearance.hasLocalProperties()
                            && source.hasLocalProperties()
                            && source.getLocalProperties().getAndCompare(ID, appearance.getLocalProperties().get(ID))) {
                        return appearance;
                    }
                }
            }

            Appearance appearance = copyBuilder.shallowCopy(source);
            appearance.setId(null);
            appearance.setSurfaceData(null);
            appearance.getLocalProperties().set(ID, source.getLocalProperties().getOrSet(ID, Integer.class, () -> id++));
            topLevelObject.getAppearances().add(new AbstractAppearanceProperty(appearance));

            return appearance;
        }
    }

    private class IdHelper extends ObjectWalker {
        private final Map<String, String> ids = new HashMap<>();
        private boolean keepOriginalIds;

        public void updateIds(AbstractGeometry geometry, boolean keepOriginalIds) {
            this.keepOriginalIds = keepOriginalIds;
            geometry.accept(this);
        }

        public String getId(String reference) {
            return ids.get(CityObjects.getIdFromReference(reference));
        }

        @Override
        public void visit(AbstractGML object) {
            if (object.getId() != null) {
                String id = DefaultIdCreator.getInstance().createId();
                if (keepOriginalIds) {
                    object.getLocalProperties().set(ID, object.getId());
                    ids.put(object.getId(), id);
                }

                object.setId(id);
            }
        }

        @Override
        public void visit(AbstractReference<?> reference) {
            reference.setHref("#" + getId(reference.getHref()));
        }
    }
}
