package io.cucumber.gherkin;

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

import io.cucumber.messages.types.Location;
import org.jspecify.annotations.Nullable;

import static io.cucumber.gherkin.Locations.COLUMN_OFFSET;
import static io.cucumber.gherkin.Locations.atColumn;
import static java.util.Objects.requireNonNull;

class ParserException extends RuntimeException {
    final @Nullable Location location;

    protected ParserException(String message) {
        super(message);
        location = null;
    }

    protected ParserException(String message, @Nullable Location location) {
        super(createMessage(message, location));
        this.location = location;
    }

    private static String createMessage(String message, @Nullable Location location) {
        if (location == null) {
            return "(-1,0): %s".formatted(message);
        }
        Integer line = location.getLine();
        Integer column = location.getColumn().orElse(0);
        return "(%s:%s): %s".formatted(line, column, message);
    }

    static final class AstBuilderException extends ParserException {
        AstBuilderException(String message, Location location) {
            super(message, location);
        }
    }

    static final class NoSuchLanguageException extends ParserException {
        NoSuchLanguageException(String language, @Nullable Location location) {
            super("Language not supported: " + language, location);
        }
    }

    static final class UnexpectedTokenException extends ParserException {

        final Token receivedToken;
        final List<String> expectedTokenTypes;
        final String stateComment;

        UnexpectedTokenException(Token receivedToken, List<String> expectedTokenTypes, String stateComment) {
            super(getMessage(receivedToken, expectedTokenTypes), getLocation(receivedToken));
            this.receivedToken = receivedToken;
            this.expectedTokenTypes = expectedTokenTypes;
            this.stateComment = stateComment;
        }

        private static String getMessage(Token receivedToken, List<String> expectedTokenTypes) {
            return "expected: %s, got '%s'".formatted(
                    String.join(", ", expectedTokenTypes), 
                    receivedToken.getTokenValue()
            );
        }

        private static Location getLocation(Token receivedToken) {
            if (receivedToken.location.getColumn().isPresent()) {
                return receivedToken.location;
            }
            int column = COLUMN_OFFSET + requireNonNull(receivedToken.line).getIndent();
            return atColumn(receivedToken.location, column);
        }
    }

    static final class UnexpectedEOFException extends ParserException {
        final String stateComment;
        final List<String> expectedTokenTypes;

        UnexpectedEOFException(Token receivedToken, List<String> expectedTokenTypes, String stateComment) {
            super(getMessage(expectedTokenTypes), receivedToken.location);
            this.expectedTokenTypes = expectedTokenTypes;
            this.stateComment = stateComment;
        }

        private static String getMessage(List<String> expectedTokenTypes) {
            return "unexpected end of file, expected: %s".formatted(
                    String.join(", ", expectedTokenTypes));
        }
    }

    static final class CompositeParserException extends ParserException {
        final List<ParserException> errors;

        CompositeParserException(List<ParserException> errors) {
            super(getMessage(errors));
            this.errors = List.copyOf(errors);
        }

        private static String getMessage(List<ParserException> errors) {
            return "Parser errors:\n" + errors.stream()
                    .map(Throwable::getMessage)
                    .collect(Collectors.joining("\n"));
        }
    }
}
