/*
 * 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.rl.ast;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import com.google.common.collect.Lists;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import nz.org.riskscape.dsl.Token;
import nz.org.riskscape.engine.util.Pair;
import nz.org.riskscape.rl.TokenTypes;

/**
 * Property access represents an indexing of the scope or another expression, e.g. `foo`, `foo.bar` or `foo().bar` are
 * all property access expressions.
 *
 * PropertyAccess can also be a 'select-all' style property, which can be used within a {@link StructDeclaration} to
 * select all of the output members or another expression in to a struct, e.g `{foo.*}` or `{foo().*}`.
 */
@RequiredArgsConstructor @EqualsAndHashCode(callSuper = false) @Getter @Setter
public final class PropertyAccess extends BaseExpr {

  public static PropertyAccess of(String... identifiers) {
    return of(Arrays.asList(identifiers));
  }

  public static PropertyAccess of(List<String> identifiers) {
    List<Token> tokens = Lists.newArrayList();
    for (String identifier: identifiers) {
      tokens.add(Token.token(TokenTypes.IDENTIFIER, identifier));
    }
    return new PropertyAccess(Optional.empty(), tokens);
  }

  /**
   * If here, this is the thing that is being accessed, e.g. in `foo(bar).baz` then `foo(bar)` is the receiver.  Without
   * this, the property is looked up against the scope, which is usually a struct.
   */
  private final Optional<Expression> receiver;
  private final List<Token> identifiers;

  public PropertyAccess and(String additionalPath) {
    List<Token> newIdentifiers = Lists.newArrayList(this.identifiers);
    newIdentifiers.add(Token.token(TokenTypes.IDENTIFIER, additionalPath));
    return new PropertyAccess(receiver, newIdentifiers);
  }

  @Override
  protected void appendSource(StringBuilder appendTo) {
    receiver.ifPresent(r -> {
      BaseAST.appendSource(r, appendTo);
      appendTo.append(".");
    });

    join(appendTo, identifiers, (sb, t) -> sb.append(t.rawValue()), ".", "", "");
  }

  public String getAccessString() {
    return identifiers.stream().map(t -> t.value).collect(Collectors.joining("."));
  }

  /**
   * @return the first token from this {@link PropertyAccess} objects list of identifiers, e.g. return `foo` from
   * `foo.bar.baz`
   */
  public Token getFirstIdentifier() {
    return identifiers.get(0);
  }

  /**
   * @return the last token from this {@link PropertyAccess} objects list of identifiers, e.g. return `baz` from
   * `foo.bar.baz`.  Returns the same as {@link #getFirstIdentifier()} if only one identifier exists.
   */
  public Token getLastIdentifier() {
    return identifiers.get(identifiers.size() - 1);
  }

  /**
   * @return true if this is the most basic of property access expressions, e.g. just identifies a property, e.g `foo`
   * but not `foo.bar` and not `bar().foo`
   */
  public boolean isSingleIdentifier() {
    return !receiver.isPresent() && identifiers.size() == 1;
  }

  @Override
  public <T, R> R accept(ExpressionVisitor<T, R> visitor, T data) {
    return visitor.visit(this, data);
  }

  @Override
  public Optional<Pair<Token, Token>> getBoundary() {
    return Optional.of(
        Pair.of(
            getReceiver().flatMap(r -> r.getBoundary().map(Pair::getLeft)).orElse(getFirstIdentifier()),
            getLastIdentifier()
        )
    );
  }

  /**
   * @return true if this expression has a trailing select-all, e.g. `foo.bar.*`
   */
  public boolean isTrailingSelectAll() {
    return getIdentifiers().get(getIdentifiers().size() - 1).getValue().equals("*");
  }

  /**
   * @return true if the expression is in the form `EXPRESSION INDEX ASTERISK`, e.g `foo().*``
   */
  public boolean isReceiverSelectAll() {
    return receiver.isPresent() && identifiers.size() == 1 && getFirstIdentifier().getValue().equals("*");
  }

  @Override
  public boolean isLiteral() {
    // if the receiver is literal, then property access in to it is also literal.  Note that there are cases where
    // property access might be for a property that isn't literal, but the ast isn't smart enough to distinguish these
    // cases
    return receiver.map(Expression::isLiteral).orElse(false);
  }
}
