/*
 * 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.Appearance;
import org.citygml4j.core.model.core.AbstractFeature;
import org.citygml4j.core.model.core.AbstractSpaceBoundary;
import org.citygml4j.core.model.core.ImplicitGeometry;
import org.citygml4j.core.model.relief.AbstractReliefComponent;
import org.citygml4j.core.visitor.ObjectWalker;
import org.citygml4j.tools.log.Logger;
import org.xmlobjects.gml.model.base.AbstractInlineOrByReferenceProperty;
import org.xmlobjects.gml.model.common.CoordinateListProvider;
import org.xmlobjects.gml.model.geometry.*;
import org.xmlobjects.gml.model.geometry.primitives.*;

import java.util.List;
import java.util.Locale;
import java.util.Map;

public class HeightChanger {
    private final Logger log = Logger.getInstance();
    private final double offset;
    private final HeightProcessor heightProcessor = new HeightProcessor();
    private final ImplicitGeometryResolver resolver = new ImplicitGeometryResolver();

    private Map<String, AbstractGeometry> templates;
    private Mode mode = Mode.RELATIVE;
    private double correction;

    public enum Mode {
        RELATIVE,
        ABSOLUTE;

        @Override
        public String toString() {
            return name().toLowerCase(Locale.ROOT);
        }
    }

    private HeightChanger(double offset) {
        this.offset = offset;
    }

    public static HeightChanger of(double offset) {
        return new HeightChanger(offset);
    }

    public HeightChanger withTemplateGeometries(Map<String, AbstractGeometry> templates) {
        this.templates = templates;
        return this;
    }

    public HeightChanger withMode(Mode mode) {
        this.mode = mode;
        return this;
    }

    public void changeHeight(AbstractFeature feature) {
        if (offset != 0 || mode != Mode.RELATIVE) {
            if (templates != null && !templates.isEmpty()) {
                feature.accept(resolver);
            }

            double minimumHeight = mode == Mode.ABSOLUTE ?
                    feature.computeEnvelope().getLowerCorner().getValue().get(2) :
                    0;

            correction = offset - minimumHeight;
            if (correction != 0) {
                feature.accept(heightProcessor);
            } else {
                log.debug("Skipping " + CityObjects.getObjectSignature(feature) + " because the height does not " +
                        "change for the given offset.");
            }
        }
    }

    private class HeightProcessor extends ObjectWalker {

        @Override
        public void visit(AbstractFeature feature) {
            if (feature.getBoundedBy() != null && feature.getBoundedBy().isSetEnvelope()) {
                Envelope envelope = feature.getBoundedBy().getEnvelope();
                List<Double> coordinates = changeCoordinates(envelope);
                envelope.getLowerCorner().setValue(coordinates.subList(0, 3));
                envelope.getUpperCorner().setValue(coordinates.subList(3, 6));
            }

            super.visit(feature);
        }

        @Override
        public void visit(Point point) {
            point.setPos(changePosition(point));
        }

        @Override
        public void visit(Curve curve) {
            if (curve.getSegments() != null && curve.getSegments().isSetObjects()) {
                for (AbstractCurveSegment segment : curve.getSegments().getObjects()) {
                    if (segment instanceof LineStringSegment) {
                        LineStringSegment lineString = (LineStringSegment) segment;
                        lineString.getControlPoints().setPosList(changePositionList(lineString));
                    }
                }
            }
        }

        @Override
        public void visit(LineString lineString) {
            lineString.getControlPoints().setPosList(changePositionList(lineString));
        }

        @Override
        public void visit(LinearRing linearRing) {
            linearRing.getControlPoints().setPosList(changePositionList(linearRing));
        }

        @Override
        public void visit(ImplicitGeometry implicitGeometry) {
            if (implicitGeometry.getReferencePoint() != null
                    && implicitGeometry.getReferencePoint().getObject() != null) {
                visit(implicitGeometry.getReferencePoint().getObject());
            }
        }

        @Override
        public void visit(AbstractReliefComponent reliefComponent) {
            visit((AbstractSpaceBoundary) reliefComponent);
        }

        @Override
        public void visit(AbstractInlineOrByReferenceProperty<?> property) {
            if (property.isSetInlineObject()) {
                super.visit(property);
            }
        }

        @Override
        public void visit(Appearance appearance) {
        }

        private DirectPosition changePosition(CoordinateListProvider provider) {
            DirectPosition position = new DirectPosition(changeCoordinates(provider));
            position.setSrsDimension(3);
            return position;
        }

        private DirectPositionList changePositionList(CoordinateListProvider provider) {
            DirectPositionList positionList = new DirectPositionList(changeCoordinates(provider));
            positionList.setSrsDimension(3);
            return positionList;
        }

        private List<Double> changeCoordinates(CoordinateListProvider provider) {
            List<Double> coordinates = provider.toCoordinateList3D();
            for (int i = 2; i < coordinates.size(); i += 3) {
                coordinates.set(i, coordinates.get(i) + correction);
            }

            return coordinates;
        }
    }

    private class ImplicitGeometryResolver extends ObjectWalker {

        @Override
        public void visit(ImplicitGeometry implicitGeometry) {
            GeometryProperty<?> property = implicitGeometry.getRelativeGeometry();
            if (property != null
                    && property.getObject() == null
                    && property.getHref() != null) {
                AbstractGeometry template = templates.get(CityObjects.getIdFromReference(property.getHref()));
                if (template != null) {
                    property.setReferencedObjectIfValid(template);
                }
            }
        }
    }
}
