/*
This program has been developed by students from the bachelor Computer Science at Utrecht University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/

package cypher

import (
	"errors"
	"fmt"
	"strings"

	"git.science.uu.nl/graphpolaris/query-conversion/entity"
)

/*
ConvertQuery converts an IncomingQueryJSON object into AQL
	JSONQuery: *entity.IncomingQueryJSON, the query to be converted to AQL
	Returns: (*string, error), the AQL query and a possible error
*/
func (s *Service) ConvertQuery(JSONQuery *entity.IncomingQueryJSON) (*string, error) {

	// Check to make sure all indexes exist
	// How many entities are there
	numEntities := len(JSONQuery.Entities) - 1
	// How many relations there are
	numRelations := len(JSONQuery.Relations) - 1

	// Make sure no entity should be returned that is outside the range of that list
	for _, e := range JSONQuery.Return.Entities {
		// If this entity references an entity that is outside the range
		if e > numEntities || e < 0 {
			return nil, errors.New("non-existing entity referenced in return")
		}
	}

	// Make sure that no relation mentions a non-existing entity
	for _, r := range JSONQuery.Relations {
		if r.EntityFrom > numEntities || r.EntityTo > numEntities {
			return nil, errors.New("non-exisiting entity referenced in relation")
		}
	}

	// Make sure no non-existing relation is tried to be returned
	for _, r := range JSONQuery.Return.Relations {
		if r > numRelations || r < 0 {
			return nil, errors.New("non-existing relation referenced in return")
		}
	}

	result := createQuery(JSONQuery)
	return result, nil
}

/*
sliceContains checks if a slice contains the input
	s: []int, the slice to check
	e: int, what you're checking for
	Return: bool, true if it contains 'e'
*/
func sliceContains(s []int, e int) bool {
	for _, a := range s {
		if a == e {
			return true
		}
	}
	return false
}

/*TrimSuffix trims the final character of a string */
func TrimSuffix(s, suffix string) string {
	if strings.HasSuffix(s, suffix) {
		s = s[:len(s)-len(suffix)]
	}
	return s
}

/*
createQuery generates a query based on the json file provided
	JSONQuery: *entity.IncomingQueryJSON, jsonQuery is a parsedJSON struct holding all the data needed to form a query
	Return: *string, a string containing the corresponding AQL query and an error
*/
func createQuery(JSONQuery *entity.IncomingQueryJSON) *string {
	// Note: Case #4, where there is an edge only query (without any entity), is not supported by frontend

	// If a modifier is used, disable the limit
	if len(JSONQuery.Modifiers) > 0 {
		JSONQuery.Limit = -1
	}

	var (
		relationsToReturn []string
		nodesToReturn     []string
		nodeUnion         string
		relationUnion     string
		queryList         [][][]int
		entityList        []int
		ret               string
	)

	for i, relation := range JSONQuery.Relations {
		var contains bool
		contains = false
		for j := range queryList {
			if sliceContains(queryList[j][0], relation.EntityFrom) || sliceContains(queryList[j][0], relation.EntityTo) {
				if !sliceContains(queryList[j][0], relation.EntityFrom) {
					queryList[j][0] = append(queryList[j][0], relation.EntityFrom)
					entityList = append(entityList, relation.EntityFrom)
				}
				if !sliceContains(queryList[j][0], relation.EntityTo) {
					queryList[j][0] = append(queryList[j][0], relation.EntityTo)
					entityList = append(entityList, relation.EntityTo)
				}
				queryList[j][1] = append(queryList[j][1], i)
				contains = true
			}
		}
		if !contains {
			queryList = append(queryList, [][]int{{relation.EntityFrom, relation.EntityTo}, {i}})
		}
	}

	for i := range queryList {
		//reset variables for the next query
		nodeUnion = ""
		relationUnion = ""
		relationsToReturn = []string{}
		for j, relationID := range queryList[i][1] {
			relationName := fmt.Sprintf("r%v", j)
			relation := JSONQuery.Relations[relationID]
			pathName := fmt.Sprintf("p%v", j)
			relationsToReturn = append(relationsToReturn, pathName)
			if relation.EntityFrom >= 0 {
				// if there is a from-node
				// create the let for this node
				fromName := fmt.Sprintf("n%v", relation.EntityFrom)

				ret += *createNodeMatch(&JSONQuery.Entities[relation.EntityFrom], &fromName)

				ret += *createRelationMatch(&relation, relationName, pathName, &JSONQuery.Entities, JSONQuery.Limit, true)
			} else if relation.EntityTo >= 0 {
				// if there is only a to-node
				toName := fmt.Sprintf("n%v", relation.EntityTo)

				ret += *createNodeMatch(&JSONQuery.Entities[relation.EntityTo], &toName)

				ret += *createRelationMatch(&relation, relationName, pathName, &JSONQuery.Entities, JSONQuery.Limit, false)
				// Add this relation to the list
			} else {
				fmt.Println("Relation-only queries are currently not supported")
				continue
			}
		}

		// Create UNION statements that create unique lists of all the nodes and relations

		// Thus removing all duplicates
		nodeUnion = "RETURN "

		for _, entityID := range queryList[i][0] {
			if sliceContains(JSONQuery.Return.Entities, entityID) {
				nodeUnion += fmt.Sprintf("n%v,", entityID)
			}
		}

		for _, relation := range relationsToReturn {
			relationUnion += fmt.Sprintf("%v,", relation)
		}

		relationUnion = TrimSuffix(relationUnion, ",")
		// hier zat een newline
		ret += nodeUnion + relationUnion + "; "
	}

	nodeSet := make(map[int]bool)
	for _, relation := range JSONQuery.Relations {
		nodeSet[relation.EntityFrom] = true
		nodeSet[relation.EntityTo] = true
	}

	// Check if the entities to return are already returned
	for _, entityIndex := range JSONQuery.Return.Entities {
		if !nodeSet[entityIndex] {
			// If not, return this node
			name := fmt.Sprintf("n%v", entityIndex)
			ret += *createNodeMatch(&JSONQuery.Entities[entityIndex], &name)
			// Add this node to the list
			nodesToReturn = append(nodesToReturn, name)
			ret += fmt.Sprintf("RETURN %v", name)
		}
	}

	ret = TrimSuffix(ret, " ")
	return &ret
}

/*
createNodeLet generates a 'LET' statement for a node related query
	node: *entity.QueryEntityStruct, node is an entityStruct containing the information of a single node,
	name: *string, is the autogenerated name of the node consisting of "n" + the index of the node
	Return: *string, a string containing a single LET-statement in AQL
*/
func createNodeMatch(node *entity.QueryEntityStruct, name *string) *string {
	// hier zat een newline
	header := fmt.Sprintf("MATCH (%v:%v) ", *name, node.Type)
	constraints := *createConstraintStatements(&node.Constraints, *name)
	ret := header + constraints
	return &ret
}

/*
createRelationLetWithFromEntity generates a 'LET' statement for relations with an 'EntityFrom' property and optionally an 'EntitiyTo' property
	relation: *entity.QueryRelationStruct, relation is a relation struct containing the information of a single relation,
	relationName: string, is the name of the relation, is the autogenerated name of the node consisting of "r" + the index of the relation,
	pathName: string, is the path of the name,
	entities: *[]entity.QueryEntityStruct, is a list of entityStructs that are needed to form the relation LET-statement
	limit: int, the limit for the number of nodes to return
	outbound: bool, checks if the relation is inbound or outbound
	Return: *string, a string containing a single LET-statement in AQL
*/
func createRelationMatch(relation *entity.QueryRelationStruct, relationName string, pathName string, entities *[]entity.QueryEntityStruct, limit int, outbound bool) *string {
	relationReturn := ""
	var relationBounds int
	if outbound {
		relationReturn = fmt.Sprintf("MATCH %v = (n%v)-[%v:%v*%v..%v]->(", pathName, relation.EntityFrom, relationName, relation.Type, relation.Depth.Min, relation.Depth.Max)
		relationBounds = relation.EntityTo

	} else {
		relationReturn = fmt.Sprintf("MATCH %v = (n%v)-[%v:%v*%v..%v]->(", pathName, relation.EntityTo, relationName, relation.Type, relation.Depth.Min, relation.Depth.Max)
		relationBounds = relation.EntityFrom
	}

	if relationBounds != -1 {
		relationReturn += fmt.Sprintf("n%v", relationBounds)
	}
	relationReturn += ")"

	constraintReturn := *createConstraintStatements(&relation.Constraints, relationName)
	// hier zat een newline
	ret := relationReturn + " " + constraintReturn

	return &ret
}