from .util import *
import redis
from .query_result import QueryResult
from .exceptions import VersionMismatchException

class Graph(object):
    """
    Graph, collection of nodes and edges.
    """

    def __init__(self, name, redis_con):
        """
        Create a new graph.
        """
        self.name = name                 # Graph key
        self.redis_con = redis_con
        self.nodes = {}
        self.edges = []
        self._labels = []                # List of node labels.
        self._properties = []            # List of properties.
        self._relationshipTypes = []     # List of relation types.
        self.version = 0                 # Graph version

    def _clear_schema(self):
        self._labels = []
        self._properties = []
        self._relationshipTypes = []

    def _refresh_schema(self):
        self._clear_schema()
        self._refresh_labels()
        self._refresh_relations()
        self._refresh_attributes()

    def _refresh_labels(self):
        lbls = self.labels()

        # Unpack data.
        self._labels = [None] * len(lbls)
        for i, l in enumerate(lbls):
            self._labels[i] = l[0]

    def _refresh_relations(self):
        rels = self.relationshipTypes()

        # Unpack data.
        self._relationshipTypes = [None] * len(rels)
        for i, r in enumerate(rels):
            self._relationshipTypes[i] = r[0]

    def _refresh_attributes(self):
        props = self.propertyKeys()

        # Unpack data.
        self._properties = [None] * len(props)
        for i, p in enumerate(props):
            self._properties[i] = p[0]

    def get_label(self, idx):
        try:
            label = self._labels[idx]
        except IndexError:
            # Refresh labels.
            self._refresh_labels()
            label = self._labels[idx]
        return label

    def get_relation(self, idx):
        try:
            relationshipType = self._relationshipTypes[idx]
        except IndexError:
            # Refresh relationship types.
            self._refresh_relations()
            relationshipType = self._relationshipTypes[idx]
        return relationshipType

    def get_property(self, idx):
        try:
            propertie = self._properties[idx]
        except IndexError:
            # Refresh properties.
            self._refresh_attributes()
            propertie = self._properties[idx]
        return propertie

    def add_node(self, node):
        """
        Adds a node to the graph.
        """
        if node.alias is None:
            node.alias = random_string()
        self.nodes[node.alias] = node

    def add_edge(self, edge):
        """
        Addes an edge to the graph.
        """

        # Make sure edge both ends are in the graph
        assert self.nodes[edge.src_node.alias] is not None and self.nodes[edge.dest_node.alias] is not None
        self.edges.append(edge)

    def commit(self):
        """
        Create entire graph.
        """
        if len(self.nodes) == 0 and len(self.edges) == 0:
            return None

        query = 'CREATE '
        for _, node in self.nodes.items():
            query += str(node) + ','

        query += ','.join([str(edge) for edge in self.edges])

        # Discard leading comma.
        if query[-1] == ',':
            query = query[:-1]

        return self.query(query)

    def flush(self):
        """
        Commit the graph and reset the edges and nodes to zero length
        """
        self.commit()
        self.nodes = {}
        self.edges = []

    def build_params_header(self, params):
        assert type(params) == dict
        # Header starts with "CYPHER"
        params_header = "CYPHER "
        for key, value in params.items():
            # If value is string add quotation marks.
            if type(value) == str:
                value = quote_string(value)
            # Value is None, replace with "null" string.
            elif value is None:
                value = "null"
            params_header += str(key) + "=" + str(value) + " "
        return params_header

    def query(self, q, params=None, timeout=None, read_only=False):
        """
        Executes a query against the graph.
        """

        # maintain original 'q'
        query = q

        # handle query parameters
        if params is not None:
            query = self.build_params_header(params) + query

        # construct query command
        # ask for compact result-set format
        # specify known graph version
        cmd = "GRAPH.RO_QUERY" if read_only else "GRAPH.QUERY"
        command = [cmd, self.name, query, "--compact", "version", self.version]

        # include timeout is specified
        if timeout:
            if not isinstance(timeout, int):
                raise Exception("Timeout argument must be a positive integer")
            command += ["timeout", timeout]

        # issue query
        try:
            response = self.redis_con.execute_command(*command)
            return QueryResult(self, response)
        except redis.exceptions.ResponseError as e:
            if "wrong number of arguments" in str(e):
                print ("Note: RedisGraph Python requires server version 2.2.8 or above")
            raise e
        except VersionMismatchException as e:
            # client view over the graph schema is out of sync
            # set client version and refresh local schema
            self.version = e.version
            self._refresh_schema()
            # re-issue query
            return self.query(q, params, timeout)

    def _execution_plan_to_string(self, plan):
        return "\n".join(plan)

    def execution_plan(self, query, params=None):
        """
        Get the execution plan for given query,
        GRAPH.EXPLAIN returns an array of operations.
        """
        
        if params is not None:
            query = self.build_params_header(params) + query
        
        plan = self.redis_con.execute_command("GRAPH.EXPLAIN", self.name, query, query)
        return self._execution_plan_to_string(plan)

    def delete(self):
        """
        Deletes graph.
        """
        self._clear_schema()
        return self.redis_con.execute_command("GRAPH.DELETE", self.name)
    
    def merge(self, pattern):
        """
        Merge pattern.
        """

        query = 'MERGE '
        query += str(pattern)

        return self.query(query)

    # Procedures.
    def call_procedure(self, procedure, *args, **kwagrs):
        args = [quote_string(arg) for arg in args]
        q = 'CALL %s(%s)' % (procedure, ','.join(args))

        y = kwagrs.get('y', None)
        if y:
            q += ' YIELD %s' % ','.join(y)

        return self.query(q)

    def labels(self):
        return self.call_procedure("db.labels").result_set

    def relationshipTypes(self):
        return self.call_procedure("db.relationshipTypes").result_set

    def propertyKeys(self):
        return self.call_procedure("db.propertyKeys").result_set

@@ -5560,16 +5560,27 @@
  timeout
+, read_only
 )%0A%0A    d
@@ -6481,16 +6481,33 @@
 ocedure,
+ read_only=False,
  *args, 
@@ -6744,16 +6744,37 @@
 .query(q
+, read_only=read_only
 )%0A%0A    d
@@ -6836,16 +6836,32 @@
 .labels%22
+, read_only=True
 ).result
@@ -6956,16 +6956,32 @@
 ipTypes%22
+, read_only=True
 ).result
@@ -7066,16 +7066,32 @@
 rtyKeys%22
+, read_only=True
 ).result

read only procedure call (#104)