package graphql;

import graphql.language.SourceLocation;
import graphql.util.FpKit;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static graphql.collect.ImmutableKit.mapAndDropNulls;

/**
 * This little helper allows GraphQlErrors to implement
 * common things (hashcode/ equals ) and to specification more easily
 */
@SuppressWarnings({"SimplifiableIfStatement", "unchecked"})
@Internal
public class GraphqlErrorHelper {

    public static Map<String, Object> toSpecification(GraphQLError error) {
        Map<String, Object> errorMap = new LinkedHashMap<>();
        errorMap.put("message", error.getMessage());
        if (error.getLocations() != null) {
            errorMap.put("locations", locations(error.getLocations()));
        }
        if (error.getPath() != null) {
            errorMap.put("path", error.getPath());
        }

        Map<String, Object> extensions = error.getExtensions();
        ErrorClassification errorClassification = error.getErrorType();
        //
        // we move the ErrorClassification into extensions which allows
        // downstream people to see them but still be spec compliant
        if (errorClassification != null) {
            if (extensions != null) {
                extensions = new LinkedHashMap<>(extensions);
            } else {
                extensions = new LinkedHashMap<>();
            }
            // put in the classification unless it's already there
            if (!extensions.containsKey("classification")) {
                extensions.put("classification", errorClassification.toSpecification(error));
            }
        }

        if (extensions != null) {
            errorMap.put("extensions", extensions);
        }
        return errorMap;
    }

    public static Object locations(List<SourceLocation> locations) {
        return mapAndDropNulls(locations, GraphqlErrorHelper::location);
    }

    /**
     * Positive integers starting from 1 required for error locations,
     * from the spec <a href="https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format">...</a>
     *
     * @param location the source location in play
     *
     * @return a value for source location of the error
     */
    public static Object location(SourceLocation location) {
        if (location == null) {
            return null;
        }
        int line = location.getLine();
        int column = location.getColumn();
        if (line < 1 || column < 1) {
            return null;
        }
        LinkedHashMap<String, Object> map = new LinkedHashMap<>(2);
        map.put("line", line);
        map.put("column", column);
        return map;
    }

    static List<GraphQLError> fromSpecification(List<Map<String, Object>> specificationMaps) {
        List<GraphQLError> list = FpKit.arrayListSizedTo(specificationMaps);
        for (Map<String, Object> specificationMap : specificationMaps) {
            list.add(fromSpecification(specificationMap));
        }
        return list;
    }

    static GraphQLError fromSpecification(Map<String, Object> specificationMap) {
        GraphQLError.Builder<?> errorBuilder = GraphQLError.newError();
        // builder will enforce not null message
        errorBuilder.message((String) specificationMap.get("message"));
        extractLocations(errorBuilder, specificationMap);
        extractPath(errorBuilder, specificationMap);
        extractExtensions(errorBuilder, specificationMap);
        return errorBuilder.build();
    }

    private static void extractPath(GraphQLError.Builder<?> errorBuilder, Map<String, Object> rawError) {
        List<Object> path = (List<Object>) rawError.get("path");
        if (path != null) {
            errorBuilder.path(path);
        }
    }

    private static void extractExtensions(GraphQLError.Builder<?> errorBuilder, Map<String, Object> rawError) {
        Map<String, Object> extensions = (Map<String, Object>) rawError.get("extensions");
        if (extensions != null) {
            errorBuilder.extensions(extensions);
            Object classification = extensions.get("classification");
            if (classification != null) {
                ErrorClassification errorClassification = ErrorClassification.errorClassification((String) classification);
                errorBuilder.errorType(errorClassification);
            }
        }

    }

    private static void extractLocations(GraphQLError.Builder<?> errorBuilder, Map<String, Object> rawError) {
        List<Object> locations = (List<Object>) rawError.get("locations");
        if (locations != null) {
            List<SourceLocation> sourceLocations = new ArrayList<>();
            for (Object locationObj : locations) {
                Map<String, Object> location = (Map<String, Object>) locationObj;
                if (location != null) {
                    Integer line = (Integer) location.get("line");
                    Integer column = (Integer) location.get("column");
                    if (line != null && column != null) {
                        sourceLocations.add(new SourceLocation(line, column));
                    }
                }
            }
            errorBuilder.locations(sourceLocations);
        }
    }

    public static int hashCode(GraphQLError dis) {
        int result = 1;
        result = 31 * result + Objects.hashCode(dis.getMessage());
        result = 31 * result + Objects.hashCode(dis.getLocations());
        result = 31 * result + Objects.hashCode(dis.getPath());
        result = 31 * result + Objects.hashCode(dis.getErrorType());
        return result;
    }

    public static boolean equals(GraphQLError dis, Object o) {
        if (dis == o) {
            return true;
        }
        if (o == null || dis.getClass() != o.getClass()) {
            return false;
        }

        GraphQLError dat = (GraphQLError) o;

        if (!Objects.equals(dis.getMessage(), dat.getMessage())) {
            return false;
        }
        if (!Objects.equals(dis.getLocations(), dat.getLocations())) {
            return false;
        }
        if (!Objects.equals(dis.getPath(), dat.getPath())) {
            return false;
        }
        return dis.getErrorType() == dat.getErrorType();
    }
}
