/*
 * 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.engine.output;

import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import lombok.Getter;

import nz.org.riskscape.engine.expr.StructMemberAccessExpression;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Struct.StructMember;
import nz.org.riskscape.engine.types.Type;

import lombok.AccessLevel;
import lombok.Setter;

/**
 * Flattens nested {@link Struct}s to allow them to be written to data sources that don't support nested data
 * structures.
 */
public class StructFlattener {

  /**
   * A Namer can provide a name for the given segments.
   *
   * Namers should take care of shortening the produced name if this is necessary.
   * {@link #shortenIfNecessary(java.util.List, int) } is only retained for backwards compatibility.
   */
  public interface Namer {
    /**
     * Get a name for the the given segments.
     * @param segments  segments to provide a name for
     * @return          name to be used for the given segments
     */
    String name(List<StructMember> segments);
  }

  /**
   * Namer that produces a name that is taken from {@link StructMember#getKey() } for each segment and
   * separated by a '.'.
   */
  public static class DotSeparatedNamer implements Namer {

    @Override
    public String name(List<StructMember> segments) {
      return segments.stream()
          .map(s -> s.getKey())
          .collect(Collectors.joining("."));
    }

  }

  /**
   * A {@link Namer} that will use the {@link StructMember#getKey() } from the last segment as the name.
   *
   * If this name has already been seen then an index is appended. E.g 'name' could become 'name_1'
   */
  public static class LastMemberNamer implements Namer {

    // used for extracting _1 suffixes from column names
    private static final Pattern ORDINAL_PATTERN = Pattern.compile("(.+)_([0-9]+)");

    private final List<String> taken = new ArrayList<>();

    @Override
    public String name(List<StructMember> segments) {
      String name = segments.get(segments.size() -1).getKey();
      while (taken.contains(name)) {
        // It's a duplicate
        Matcher matcher = ORDINAL_PATTERN.matcher(name);

        int ord;
        if (matcher.find()) {
          ord = Integer.parseInt(matcher.group(2)) + 1;
          name = matcher.group(1);
        } else {
          ord = 1;
        }

        name = name + "_" + ord;
      }

      taken.add(name);
      return name;
    }

  }

  /**
   * If there are more than ten fields with the same truncated name, this class gives up trying to
   * map them and decides to raise an exception
   */
  private static final int FIRST_DOUBLE_DIGIT = 10;

  /**
   * Matches a key that ends with a numeric type index.
   */
  private static final Pattern INDEXED_KEY_PATTERN = Pattern.compile("(\\d+)$");

  public static final String SEPARATOR = ".";

  public List<StructMapping> flatten(Struct struct) {
    return flatten(struct, new DotSeparatedNamer());
  }

  public List<StructMapping> flatten(Struct struct, Namer namer) {
    List<StructMapping> mappings = new ArrayList<>();
    flatten(mappings, struct, new ArrayList<>(), namer);
    return mappings;
  }

  private void flatten(List<StructMapping> mappings, Struct struct, List<StructMember> segments, Namer namer) {
    struct.getMembers().forEach((member) -> {
      boolean nullable = Nullable.is(member.getType());
      Type type = Nullable.strip(member.getType());
      Optional<Struct> sType = type.find(Struct.class);
      if (sType.isPresent()) {
        List<StructMember> mySegments = Lists.newArrayList(segments);
        mySegments.add(member);
        flatten(mappings, sType.get(), mySegments, namer);
      } else {
        List<StructMember> mySegments = Lists.newArrayList(segments);
        mySegments.add(member);
        mappings.add(new StructMapping(namer.name(mySegments), Nullable.ifTrue(nullable, type), mySegments));
      }
    });
  }

  /**
   * Shorten {@link StructMapping#key } if it is longer than maxLength.
   * @deprecated Ideally use a {@link Namer} that does this itself. This method is only retained for use
   *             by ShapefileWriter V1.
   */
  @Deprecated
  public void shortenIfNecessary(List<StructMapping> mappings, int maxLength) {
    mappings.stream().filter(m -> m.getKey().length() > maxLength).forEach(m -> {
      StringBuilder sb = new StringBuilder();
      List<StructMember> segments = m.getSegments();
      for (int i = 0; i < segments.size() - 1; i++) {
        sb.append(shortenKey(segments.get(i).getKey())).append(SEPARATOR);
      }
      sb.append(segments.get(segments.size() - 1).getKey());
      String key = sb.toString();
      if (key.length() > maxLength) {
        key = key.substring(0, maxLength);
      }
      m.setKey(key);
    });

    for (StructMapping entry : mappings) {
      List<StructMapping> dupes = mappings
          .stream()
          .filter(rhs -> {
            return rhs.getKey().equals(entry.getKey());
          })
          .collect(Collectors.toList());


      if (dupes.size() > 1) {
        if (dupes.size() > FIRST_DOUBLE_DIGIT) {
          // can't be bothered trying to enumerate all and then backtrack etc once past two digits
          throw new IllegalArgumentException("too many duplicated field names after truncation, giving up");
        }

        int counter = 0;
        for (StructMapping dupe : dupes) {
          String shortKey = dupe.getKey();
          if (shortKey.length() >= maxLength) {
            shortKey = shortKey.substring(0, maxLength - 1);
          }
          dupe.setKey(shortKey + Integer.toString(counter));
          counter += 1;
        }
      }
    }
  }

  /**
   * Shorten a key. Will return the initial character of key only, unless key ends with a number, in which case
   * result is inital character, plus number.
   * @param key to shorted
   * @return shortened key
   */
  String shortenKey(String key) {
    Matcher m = INDEXED_KEY_PATTERN.matcher(key);
    if (m.find()) {
      String index = m.group();
      if (key.equals(index)) {
        return key;
      }
      return key.substring(0, 1) + index;
    }
    return key.substring(0, 1);
  }

  public static class StructMapping {

    @Getter
    private final Type type;
    @Getter
    private final List<StructMember> segments;
    @Getter
    private final StructMemberAccessExpression accessExpression;
    @Getter @Setter(AccessLevel.PRIVATE)
    private String key;

    public StructMapping(String key, Type type, List<StructMember> segments) {
      this.key = key;
      this.type = type;
      this.segments = segments;
      this.accessExpression = new StructMemberAccessExpression(false, segments);
    }
  }
}
