code: /*
   * 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.AbstractGenericAttribute;
  import org.citygml4j.core.model.core.ImplicitGeometry;
  import org.citygml4j.core.util.CityGMLPatterns;
  import org.citygml4j.core.visitor.ObjectWalker;
  import org.citygml4j.tools.ExecutionException;
  import org.citygml4j.tools.log.Logger;
  import org.citygml4j.xml.CityGMLContext;
  import org.citygml4j.xml.module.Module;
  import org.citygml4j.xml.module.citygml.CityGMLModules;
  import org.citygml4j.xml.module.citygml.CoreModule;
  import org.xmlobjects.gml.model.base.AbstractReference;
  import org.xmlobjects.gml.model.feature.BoundingShape;
  import org.xmlobjects.gml.model.geometry.AbstractGeometry;
  import org.xmlobjects.gml.model.geometry.Envelope;
  import org.xmlobjects.gml.model.geometry.GeometryProperty;
  import org.xmlobjects.gml.model.geometry.SRSReference;
  
  import javax.xml.XMLConstants;
  import javax.xml.namespace.QName;
  import java.util.*;
  import java.util.function.Consumer;
  import java.util.regex.Matcher;
  
  public class StatisticsProcessor {
      private final Statistics statistics;
      private final CityGMLContext context;
      private final StatisticsWalker statisticsWalker = new StatisticsWalker();
      private final Deque<FeatureInfo> featureInfos = new ArrayDeque<>();
      private final Deque<SrsInfo> srsInfos = new ArrayDeque<>();
      private final Matcher lodMatcher = CityGMLPatterns.LOD_FROM_PROPERTY_NAME.matcher("");
      private final String DEFAULT_SRS_NAME = "unknown";
  
      private boolean computeEnvelope;
      private boolean onlyTopLevelFeatures;
      private boolean generateObjectHierarchy;
      private List<Appearance> globalAppearances;
      private Map<String, AbstractGeometry> templates;
  
      private StatisticsProcessor(Statistics statistics, CityGMLContext context) {
          this.statistics = statistics;
          this.context = context;
      }
  
      public static StatisticsProcessor of(Statistics statistics, CityGMLContext context) {
          return new StatisticsProcessor(statistics, context);
      }
  
      public StatisticsProcessor computeEnvelope(boolean computeEnvelope) {
          this.computeEnvelope = computeEnvelope;
          return this;
      }
  
      public StatisticsProcessor onlyTopLevelFeatures(boolean onlyTopLevelFeatures) {
          this.onlyTopLevelFeatures = onlyTopLevelFeatures;
          return this;
      }
  
      public StatisticsProcessor generateObjectHierarchy(boolean generateObjectHierarchy) {
          this.generateObjectHierarchy = generateObjectHierarchy;
          return this;
      }
  
      public StatisticsProcessor withGlobalAppearances(List<Appearance> globalAppearances) {
          this.globalAppearances = globalAppearances;
          return this;
      }
  
      public void process(QName element, String prefix, boolean isTopLevel, int depth, Statistics statistics) {
          statistics.addVersion(CityGMLModules.getCityGMLVersion(element.getNamespaceURI()));
  
          Module module = CityGMLModules.getModuleFor(element.getNamespaceURI());
          if (module instanceof CoreModule
                  && !statistics.hasObjects(Statistics.ObjectType.FEATURE)
                  && "CityModel".equals(element.getLocalPart())) {
              return;
          }
  
          if (!statistics.hasValidExtent()) {
              computeEnvelope = true;
          }
  
          if (module != null) {
              prefix = module.getNamespacePrefix();
          } else {
              prefix = XMLConstants.DEFAULT_NS_PREFIX.equals(prefix) ? "ade" : prefix;
          }
  
          statistics.addModule(prefix, element.getNamespaceURI());
          if (isTopLevel || !onlyTopLevelFeatures) {
              addObject(element, prefix, statistics::addFeature);
  
              if (generateObjectHierarchy) {
                  featureInfos.push(new FeatureInfo(prefix + ":" + element.getLocalPart(), depth));
                  List<String> hierarchy = new ArrayList<>();
                  featureInfos.descendingIterator()
                          .forEachRemaining(featureInfo -> hierarchy.add(featureInfo.getName()));
                  statistics.addFeatureHierarchy(hierarchy);
              }
          }
      }
  
      public void process(QName element, Appearance appearance, boolean isTopLevel) {
          if (appearance != null) {
              if (isTopLevel) {
                  statistics.setHasGlobalAppearances(true);
              }
  
              String prefix = getPrefix(element.getNamespaceURI(), "app");
              addObjectAndModule(element, prefix, statistics::addAppearance);
              statistics.addTheme(appearance.getTheme());
              appearance.accept(statisticsWalker.withAppearancePrefix(prefix));
          }
      }
  
      public void process(QName element, AbstractGeometry geometry, QName parent) {
          if (geometry != null) {
              addObjectAndModule(element, getPrefix(element.getNamespaceURI(), "gml"), statistics::addGeometry);
              getLodFromPropertyName(parent);
  
              if (computeEnvelope) {
                  statistics.getExtent(getSrsName(geometry)).include(geometry.computeEnvelope());
              }
  
              if (globalAppearances != null) {
                  processGlobalAppearances(geometry);
              }
  
              geometry.accept(statisticsWalker);
          }
      }
  
      public void process(QName element, ImplicitGeometry implicitGeometry, QName parent) throws ExecutionException {
          if (implicitGeometry != null) {
              addObjectAndModule(element, getPrefix(element.getNamespaceURI(), "core"), statistics::addGeometry);
              getLodFromPropertyName(parent);
  
              if (computeEnvelope) {
                  GeometryProperty<?> property = implicitGeometry.getRelativeGeometry();
                  if (property != null
                          && property.getObject() == null
                          && property.getHref() != null) {
                      if (templates == null) {
                          lazyLoadTemplates();
                      }
  
                      AbstractGeometry template = templates.get(CityObjects.getIdFromReference(property.getHref()));
                      if (template != null) {
                          property.setReferencedObjectIfValid(template);
                          if (globalAppearances != null) {
                              processGlobalAppearances(template);
                          }
                      }
                  }
  
                  String srsName = implicitGeometry.getReferencePoint() != null
                          && implicitGeometry.getReferencePoint().getObject() != null ?
                          getSrsName(implicitGeometry.getReferencePoint().getObject()) :
                          getCurrentSrsName();
  
                  statistics.getExtent(srsName).include(implicitGeometry.computeEnvelope());
              }
  
              statistics.setHasImplicitGeometries(true);
              implicitGeometry.accept(statisticsWalker);
          }
      }
  
      public void process(AbstractGenericAttribute<?> genericAttribute) {
          if (genericAttribute != null) {
              statistics.addGenericAttribute(genericAttribute.getName(), genericAttribute.getClass().getSimpleName());
          }
      }
  
      public void process(BoundingShape boundingShape, int depth, boolean isCityModel, Statistics statistics) {
          if (boundingShape != null && boundingShape.isSetEnvelope()) {
              Envelope envelope = boundingShape.getEnvelope();
  
              if (envelope.getSrsName() != null) {
                  srsInfos.push(new SrsInfo(envelope.getSrsName(), depth));
                  statistics.addReferenceSystem(envelope.getSrsName());
              }
  
              if (isCityModel && !computeEnvelope) {
                  statistics.getExtent(getSrsName(envelope)).include(envelope);
              }
          }
      }
  
      public void updateDepth(int depth) {
          if (generateObjectHierarchy
                  && !featureInfos.isEmpty()
                  && featureInfos.peek().getDepth() == depth + 1) {
              featureInfos.pop();
          }
  
          if (!srsInfos.isEmpty()
                  && srsInfos.peek().getDepth() == depth + 1) {
              srsInfos.pop();
          }
      }
  
      private void addObjectAndModule(QName element, String prefix, Consumer<String> consumer) {
          statistics.addModule(prefix, element.getNamespaceURI());
          addObject(element, prefix, consumer);
      }
  
      private void addObject(QName element, String prefix, Consumer<String> consumer) {
          consumer.accept(prefix + ":" + element.getLocalPart());
      }
  
      private void getLodFromPropertyName(QName property) {
          if (property != null) {
              lodMatcher.reset(property.getLocalPart());
              if (lodMatcher.matches()) {
                  statistics.addLod(Integer.parseInt(lodMatcher.group(1)));
              }
          }
      }
  
      private String getPrefix(String namespaceURI, String defaultPrefix) {
          Module module = CityGMLModules.getModuleFor(namespaceURI);
          return module != null ? module.getNamespacePrefix() : defaultPrefix;
      }
  
      private String getCurrentSrsName() {
          if (!srsInfos.isEmpty()) {
              return srsInfos.peek().getSrsName();
          } else {
              statistics.addReferenceSystem(DEFAULT_SRS_NAME);
              return DEFAULT_SRS_NAME;
          }
      }
  
      private String getSrsName(SRSReference srsReference) {
          return srsReference != null && srsReference.getSrsName() != null ?
                  srsReference.getSrsName() :
                  getCurrentSrsName();
      }
  
      private void lazyLoadTemplates() throws ExecutionException {
          Logger.getInstance().debug("Reading implicit geometries from input file.");
          templates = GlobalObjectsReader.onlyImplicitGeometries()
                  .read(statistics.getFile(), context)
                  .getTemplateGeometries();
      }
  
      private void processGlobalAppearances(AbstractGeometry geometry) {
          GlobalAppearanceResolver resolver = GlobalAppearanceResolver.of(geometry);
          for (Appearance globalAppearance : globalAppearances) {
              Appearance appearance = resolver.resolve(globalAppearance);
              if (appearance != null) {
                  QName name = globalAppearance.getLocalProperties().get(GlobalObjects.NAME, QName.class);
                  process(name, appearance, true);
              }
          }
      }
  
      private class StatisticsWalker extends ObjectWalker {
          private String appearancePrefix;
  
          public StatisticsWalker withAppearancePrefix(String appearancePrefix) {
              this.appearancePrefix = appearancePrefix;
              return this;
          }
  
          @Override
          public void visit(AbstractGeometry geometry) {
              statistics.addReferenceSystem(geometry.getSrsName());
              super.visit(geometry);
          }
  
          @Override
          public void visit(AbstractSurfaceData surfaceData) {
              statistics.addAppearance(appearancePrefix + ":" + surfaceData.getClass().getSimpleName());
          }
  
          @Override
          public void visit(AbstractTexture texture) {
              statistics.setHasTextures(true);
              super.visit(texture);
          }
  
          @Override
          public void visit(X3DMaterial x3dMaterial) {
              statistics.setHasMaterials(true);
              super.visit(x3dMaterial);
          }
      }
  
      private static class GlobalAppearanceResolver extends ObjectWalker {
          private final Set<String> targets = new HashSet<>();
          private final Set<AbstractSurfaceData> surfaceData = Collections.newSetFromMap(new IdentityHashMap<>());
  
          public static GlobalAppearanceResolver of(AbstractGeometry geometry) {
              GlobalAppearanceResolver resolver = new GlobalAppearanceResolver();
              geometry.accept(resolver);
              return resolver;
          }
  
          public Appearance resolve(Appearance globalAppearance) {
              globalAppearance.accept(this);
              if (!surfaceData.isEmpty()) {
                  Appearance appearance = new Appearance();
                  appearance.setTheme(globalAppearance.getTheme());
                  surfaceData.stream()
                          .map(AbstractSurfaceDataProperty::new)
                          .forEach(appearance.getSurfaceData()::add);
  
                  surfaceData.clear();
                  return appearance;
              } else {
                  return null;
              }
          }
  
          @Override
          public void visit(AbstractGeometry geometry) {
              if (geometry.getId() != null) {
                  targets.add(geometry.getId());
                  targets.add("#" + geometry.getId());
              }
          }
  
          @Override
          public void visit(AbstractReference<?> reference) {
              if (reference.getHref() != null && targets.contains(reference.getHref())) {
                  AbstractSurfaceData surfaceData = reference.getParent(AbstractSurfaceData.class);
                  if (surfaceData != null) {
                      this.surfaceData.add(surfaceData);
                  }
              }
          }
      }
  
      private static class FeatureInfo {
          private final String name;
          private final int depth;
  
          FeatureInfo(String name, int depth) {
              this.name = name;
              this.depth = depth;
          }
  
          public String getName() {
              return name;
          }
  
          public int getDepth() {
              return depth;
          }
      }
  
      private static class SrsInfo {
          private final String srsName;
          private final int depth;
  
          SrsInfo(String srsName, int depth) {
              this.srsName = srsName;
              this.depth = depth;
          }
  
          public String getSrsName() {
              return srsName;
          }
  
          public int getDepth() {
              return depth;
          }
      }
  }
  