/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */

package nz.org.riskscape.cpython;

import java.io.DataOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.google.common.collect.ImmutableMap;

import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.types.Geom;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Struct.StructBuilder;
import nz.org.riskscape.engine.types.Struct.StructMember;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.TypeVisitor;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.types.WrappingType;
import nz.org.riskscape.engine.util.Pair;

/**
 * Adds rudimentary handling of unexpected null values before we try to serialize the
 * data (i.e. thus avoiding an NPE). The main source of an unexpected null would be the input data,
 * e.g. we read an integer attribute from the shapefile, but the value stored is null.
 *
 * Note that this does not perfectly avoid every possible NPE when serializing. It just aims to be
 * 'good enough' to avoid falling over when the exposure-layer shapefile contains null values.
 * Thus, we don't guarantee null-safe handling of nullable structs for example
 * (yes, a hazard vector layer could still have null values, but users are more likely to define a
 * type, or pick a single attribute, for the hazard-layer).
 */
public class CPythonSerializer {

  /**
   * Type name mappings used for python type info serialization - this just cleans them up a little in error messages,
   * BUT it makes them inconsistent with their names in java error messages... sooo...
   * FIXME get this to be a riskscape type expression, or at least equivalent-ish?
   */
  public static final Map<Class<?>, String> TYPE_ALIASES = ImmutableMap.of(
    RSList.class, "List",
    Geom.class, "Geometry"
  );

  /**
   * Turn the type into a string form that the serializer.py script can parse.
   */
  public static String serializerType(Type type) {

    StringBuilder builder = new StringBuilder();

    type.visit(new TypeVisitor<Void, Void>() {

      @Override
      public Void atomicType(Type simpleType, Void ignored) {
        append(simpleType);
        return null;
      }

      @Override
      public Void compoundType(Type type, List<Pair<Type, ?>> children, Void ignored) {

        // for now, we skip all the metadata types and focus on the thing with the value
        if (type instanceof WrappingType) {
          return ((WrappingType) type).getUnderlyingType().visit(this, ignored);
        }

        append(type).append("[");

        boolean first = true;
        for (Pair<Type, ?> pair : children) {
          if (!first) {
            builder.append(", ");
          }
          first = false;
          Type childType = pair.getLeft();
          Object meta = pair.getRight();

          if (meta != TypeVisitor.NO_META) {
            builder.append(meta.toString()).append("=>");
          }

          childType.visit(this, null);
        }

        builder.append("]");

        return null;
      }

      private StringBuilder append(Type type) {
        builder.append(TYPE_ALIASES.getOrDefault(type.getClass(), type.getClass().getSimpleName()));
        return builder;
      }
    }, null);

    return builder.toString();
  }

  private static final Map<Type, Type> NULL_SAFE_MAPPINGS = ImmutableMap.of(
      Types.BOOLEAN, Nullable.BOOLEAN,
      Types.INTEGER, Nullable.INTEGER,
      Types.FLOATING, Nullable.FLOATING,
      // note that null text in a shapefile appear to always be read in as an empty
      // string, so this mapping might be unnecessary but I've left it in for completeness
      Types.TEXT, Nullable.TEXT
  );

  /**
   * Serializes the given type/value to the given output stream with additional null-safety checks.
   */
  public void serialize(DataOutputStream output, Type type, Object value) throws IOException {
    Optional<Struct> structOr = type.find(Struct.class);

    if (structOr.isPresent()) {
      // any struct member could be null, so check them all
      serializeStruct(output, structOr.get(), value);

    } else if (type instanceof WrappingType) {
      serialize(output, ((WrappingType) type).getUnderlyingType(), value);

    } else {
      nullSafeSimpleType(type).toBytes(output, value);
    }
  }

  private void serializeStruct(DataOutputStream output, Struct struct, Object toWrite) throws IOException {
    Tuple tuple = (Tuple) toWrite;

    // recurse the struct members and make sure they are all serialized in a null-safe manner
    for (StructMember member : struct.getMembers()) {
      serialize(output, member.getType(), tuple.fetch(member));
    }
  }

  /**
   * Returns an equivalent type that will handle unexpected null values in the underlying input data.
   * @return either the same type, or a new equivalent type that is null-safe
   */
  public static Type nullSafeType(Type type) {
    Optional<Struct> structOr = type.find(Struct.class);

    if (structOr.isPresent()) {
      // any struct member could be null, so check them all
      return nullSafeStruct(structOr.get());

    } else if (type instanceof WrappingType) {
      return nullSafeType(((WrappingType) type).getUnderlyingType());

    } else {
      return nullSafeSimpleType(type);
    }
  }

  private static Type nullSafeSimpleType(Type type) {
    for (Map.Entry<Type, Type> typeMapping : NULL_SAFE_MAPPINGS.entrySet()) {
      if (typeMapping.getKey().equals(type)) {
        return typeMapping.getValue();
      }
    }
    // it's not a simple type we care about, so return it as is
    return type;
  }

  private static Type nullSafeStruct(Struct struct) {
    StructBuilder builder = Struct.builder();

    // recurse the struct members and make sure they are all null-safe
    for (StructMember member : struct.getMembers()) {
      builder.add(member.getKey(), nullSafeType(member.getType()));
    }
    return builder.build();
  }
}
