#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# vim: sw=4 ts=4 expandtab ai
#
# Copyright (c) 2013-2014 Intel, Inc.
# License: GPLv2
# Author: Alexander Kanevskiy <alexander.kanevskiy@intel.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2,
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.

import urlparse
import urllib2
import json
import types


class GerritREST(object):
    """Access Gerrit via REST interface"""
    def __init__(self, baseurl, username, password):
        super(GerritREST, self).__init__()
        self.baseurl = baseurl
        if not self.baseurl.endswith("/"):
            self.baseurl += "/"
        mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
        mgr.add_password(None, baseurl, username, password)
        self._opener_auth_handler = urllib2.HTTPDigestAuthHandler(mgr)
        self._opener = urllib2.build_opener(self._opener_auth_handler)

    def _do_http_call(self, relative_url, data=None, method='GET', headers={}):
        """Makes HTTP request with digest authentication"""
        request = urllib2.Request(urlparse.urljoin(self.baseurl, relative_url), data=data, headers=headers)
        if method == "PUT":
            request.get_method = lambda: 'PUT'
        elif method == "DELETE":
            request.get_method = lambda: 'DELETE'
        elif method == "POST" and not data:
            request.get_method = lambda: 'POST'

        try:
            fobj = self._opener.open(request)
            content = fobj.read()
            if content[:5] == ")]}'\n":
                content = content[5:]
            return (content, fobj.getcode(), fobj.info())
        except urllib2.HTTPError, httperr:
            self._opener_auth_handler.reset_retry_count() # Hack
            #return (None, httperr.code, httperr.headers)
            return (None, httperr.code, httperr)

    def _do_json_call(self, relative_url, data=None, method='GET', headers={}):
        """Makes HTTP request, and returns parsed json result"""
        ret = self._do_http_call(relative_url, data, method, headers)
        if ret[0]:
            return json.loads(ret[0])
        else:
            return None

    @staticmethod
    def quote_id(oid):
        "Returns encoded project or group name if it contains /"
        return urllib2.quote(oid, "") if '/' in oid else oid

    @staticmethod
    def change_id(changeid, project=None, branch=None):
        "Returns constructed change_id with project / branch info"
        if project and branch:
            return "%s~%s~%s" % (GerritREST.quote_id(project), branch, changeid)
        else:
            return "%s" % changeid

    def get_projects(self, description=False, parents=False, prefix=None, branches=None):
        """
        GET /projects/
        """
        opts = []
        if description:
            opts.append("d")
        if parents:
            opts.append("t")
        if prefix:
            opts.append("p=%s" % (urllib2.quote(prefix, "") \
                if '/' in prefix else prefix))
        if branches:
            if not isinstance(branches, (types.ListType, types.TupleType)):
                branches = [branches]
            for branch in branches:
                opts.append("b=%s" % urllib2.quote(branch, ""))
        if opts:
            return self._do_json_call("a/projects/?%s" % "&".join(opts))
        else:
            return self._do_json_call("a/projects/")


    def get_project(self, project):
        """
        GET /projects/{project-name}
        """
        if not project:
            return None
        return self._do_json_call("a/projects/%s" % self.quote_id(project))

    def create_project(self, project, parent=None, description=None, \
        permissions_only=None, create_empty_commit=None, submit_type=None, \
        branches=None, owners=None, use_contributor_agreements=None, \
        use_signed_off_by=None, use_content_merge=None, require_change_id=None):
        """
        PUT /projects/{project-name}
        name: The name of the project (not encoded).
        parent: The name of the parent project.
        description: The description of the project.
        permissions_only: false if not set
        create_empty_commit: false if not set
        submit_type: optional (*MERGE_IF_NECESSARY, REBASE_IF_NECESSARY,
                        FAST_FORWARD_ONLY, MERGE_ALWAYS, CHERRY_PICK).
        branches: A list of branches that should be initially created.
             For the branch names the refs/heads/ prefix can be omitted.
        owners: A list of groups that should be assigned as project owner.
        use_contributor_agreements: Whether contributor agreements should be
                                used for the project (TRUE, FALSE, *INHERIT).
        use_signed_off_by: Whether the usage of Signed-Off-By footers is
                            required for the project (TRUE, FALSE, *INHERIT).
        use_content_merge: Whether content merge should be enabled for the
                            project (TRUE, FALSE, *INHERIT).
                            FALSE, if the submit_type is FAST_FORWARD_ONLY.
        require_change_id: Whether the usage of Change-Ids is required for
                            the project (TRUE, FALSE, *INHERIT).
        """
        if not project:
            return None
        info = {}
        info['name'] = project
        if parent:
            info['parent'] = parent
        if description:
            info['description'] = description
        if permissions_only is not None:
            info['permissions_only'] = permissions_only
        if create_empty_commit is not None:
            info['create_empty_commit'] = create_empty_commit
        if submit_type:
            info['submit_type'] = submit_type
        if branches:
            info['branches'] = branches
        if owners:
            info['owners'] = owners
        if use_contributor_agreements:
            info['use_contributor_agreements'] = use_contributor_agreements
        if use_signed_off_by:
            info['use_signed_off_by'] = use_signed_off_by
        if use_content_merge:
            info['use_content_merge'] = use_content_merge
        if require_change_id:
            info['require_change_id'] = require_change_id
        return self._do_json_call( \
                "a/projects/%s" % self.quote_id(project), \
                method='PUT', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def get_project_description(self, project):
        """
        GET /projects/{project-name}/description
        """
        if not project:
            return None
        return self._do_json_call("a/projects/%s/description" % self.quote_id(project))

    def set_project_description(self, project, description=None, commit_message=None):
        """
        PUT /projects/{project-name}/description
        Content-Type: application/json;charset=UTF-8
        {
            "description": optional, The project description.
                        The project description will be deleted if not set.
            "commit_message": optional,  Message that should be used to commit
            the change of the project description in the project.config file to
            the refs/meta/config branch.
        }
        """
        info = {}
        if description:
            info['description'] = description
        if commit_message:
            info['commit_message'] = commit_message
        return self._do_json_call( \
                "a/projects/%s/description" % self.quote_id(project), \
                method='PUT', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def delete_project_description(self, project):
        """
        DELETE /projects/{project-name}/description
        """
        if not project:
            return None
        return self._do_json_call("a/projects/%s/description" % \
                                    self.quote_id(project), method='DELETE')

    def get_project_parent(self, project):
        """
        GET /projects/{project-name}/parent
        """
        if not project:
            return None
        return self._do_json_call("a/projects/%s/parent" % self.quote_id(project))

    def set_project_parent(self, project, parent, commit_message=None):
        """
        PUT /projects/{project-name}/parent
        Content-Type: application/json;charset=UTF-8

        {
            "parent": The name of the parent project.
            "commit_message": optional, Message that should be used to commit
                the change of the project parent in the project.config file to
                the refs/meta/config branch.
        }
        """
        info = {}
        if parent:
            info['parent'] = parent
        if commit_message:
            info['commit_message'] = commit_message
        return self._do_json_call( \
                "a/projects/%s/parent" % self.quote_id(project), \
                method='PUT', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def get_project_head(self, project):
        """
        GET /projects/{project-name}/HEAD
        """
        if not project:
            return None
        return self._do_json_call("a/projects/%s/HEAD" % self.quote_id(project))

    def set_project_head(self, project, head):
        """
        PUT /projects/{project-name}/HEAD
        ref: The ref to which HEAD should be set,
            the refs/heads prefix can be omitted.
        """
        info = {}
        if head:
            info['ref'] = head
        return self._do_json_call( \
                "a/projects/%s/HEAD" % self.quote_id(project), \
                method='PUT', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def get_project_statistics(self, project):
        """
        GET /projects/{project-name}/statistics.git
        """
        if not project:
            return None
        return self._do_json_call("a/projects/%s/statistics.git" % \
                                    self.quote_id(project))

    def run_project_gc(self, project):
        """
        POST /projects/{project-name}/gc
        returned data is not JSON!
        """
        if not project:
            return None
        return self._do_http_call("a/projects/%s/gc" % \
                                    self.quote_id(project), method='POST')[0]

    def get_groups(self, members=False, includes=False, project=None, user=None):
        """
        GET /groups/
        TODO: q parameter
        """
        opts = []
        if members:
            opts.append("o=MEMBERS")
        if includes:
            opts.append("o=INCLUDES")
        if project:
            opts.append("p=%s" % self.quote_id(project))
        if user:
            opts.append("u=%s" % user)
        if opts:
            return self._do_json_call("a/groups/?%s" % "&".join(opts))
        else:
            return self._do_json_call("a/groups/")

    def get_group(self, group):
        """
        GET /groups/{group-id}
        """
        if not group:
            return None
        return self._do_json_call("a/groups/%s" % group)


    def create_group(self, group, description=None, visible_to_all=None, owner=None):
        """
        PUT /groups/{group-name}
        all parameters optional
        Content-Type: application/json;charset=UTF-8

        {
            "description": "contains all committers for MyProject",
            "visible_to_all": true,
            "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc"
        }
        """
        if not group:
            return None
        info = {}
        if description:
            info['description'] = description
        if visible_to_all is not None:
            info['visible_to_all'] = visible_to_all
        if owner:
            info['owner_id'] = owner
        return self._do_json_call( \
                "a/groups/%s" % self.quote_id(group), \
                method='PUT', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def get_group_detail(self, group):
        """
        GET /groups/{group-id}/detail
        """
        if not group:
            return None
        return self._do_json_call("a/groups/%s/detail" % self.quote_id(group))


    def get_group_name(self, group):
        """
        GET /groups/{group-id}/name
        """
        if not group:
            return None
        return self._do_json_call("a/groups/%s/name" % self.quote_id(group))

    def set_group_name(self, group, new_name):
        """
        PUT /groups/{group-id}/name
        Content-Type: application/json;charset=UTF-8

        {
            "name": "My-Project-Committers"
        }
        """
        if not group or not new_name:
            return None
        info = {'name': new_name}
        return self._do_json_call( \
                "a/groups/%s" % self.quote_id(group), \
                method='PUT', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def get_group_description(self, group):
        """
        GET /groups/{group-id}/description
        """
        if not group:
            return None
        return self._do_json_call("a/groups/%s/description" % \
                                    self.quote_id(group))

    def set_group_description(self, group, new_description):
        """
        PUT /groups/{group-id}/description
        Content-Type: application/json;charset=UTF-8

        {
            "description": "The committers of MyProject."
        }
        """
        if not group:
            return None
        info = {'description': new_description}
        return self._do_json_call( \
                "a/groups/%s/description" % self.quote_id(group), \
                method='PUT', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def delete_group_description(self, group):
        """
        DELETE /groups/{group-id}/description
        """
        if not group:
            return None
        return self._do_json_call("a/groups/%s/description" % \
                                    self.quote_id(group), method='DELETE')

    def get_group_options(self, group):
        """
        GET /groups/{group-id}/options
        """
        if not group:
            return None
        return self._do_json_call("a/groups/%s/options" % self.quote_id(group))

    def set_group_options(self, group, visible_to_all=None):
        """
        PUT /groups/{group-id}/options
        Content-Type: application/json;charset=UTF-8

        {
            "visible_to_all": true
        }
        """
        if not group or visible_to_all is None:
            return None
        info = {'visible_to_all': visible_to_all}
        return self._do_json_call( \
                "a/groups/%s/options" % self.quote_id(group), \
                method='PUT', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def get_group_owner(self, group):
        """
        GET /groups/{group-id}/owner
        """
        if not group:
            return None
        return self._do_json_call("a/groups/%s/owner" % self.quote_id(group))

    def set_group_owner(self, group, new_owner):
        """
        PUT /groups/{group-id}/owner

        Content-Type: application/json;charset=UTF-8

        {
            "owner": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
        }
        """
        if not group:
            return None
        info = {'owner': new_owner}
        return self._do_json_call( \
                "a/groups/%s/owner" % self.quote_id(group), \
                method='PUT', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def get_group_member(self, group, member):
        """
        GET /groups/{group-id}/members/{account-id}
        """
        if not group or not member:
            return None
        return self._do_json_call("a/groups/%s/member/%s" % \
                                    (self.quote_id(group), member))

    def add_group_member(self, group, member):
        """
        PUT /groups/{group-id}/members/{account-id}
        """
        if not group or not member:
            return None
        return self._do_json_call("a/groups/%s/members/%s" % \
                        (self.quote_id(group), member), method='PUT', data="")

    def delete_group_member(self, group, member):
        """
        DELETE /groups/{group-id}/members/{account-id}
        """
        if not group or not member:
            return None
        return self._do_json_call("a/groups/%s/members/%s" % \
                            (self.quote_id(group), member), method='DELETE')

    def get_group_members(self, group, recursive=None):
        """
        GET /groups/{group-id}/members/
        GET /groups/{group-id}/members/?recursive
        """
        if not group:
            return None
        options = "?recursive" if recursive else ""
        return self._do_json_call("a/groups/%s/members/%s" % \
                                    (self.quote_id(group), options))

    def add_group_members(self, group, members):
        """
        POST /groups/{group-id}/members.add
        or
        POST /groups/{group-id}/members
        members: A list of account ids that identify the accounts that should
                be added or deleted.
        TODO:
        _one_member: The id of one account that should be added or deleted.
          Content-Type: application/json;charset=UTF-8

          {
            "members": {
              "jane.roe@example.com",
              "john.doe@example.com"
            }
          }
        """
        if not group or not members:
            return None
        info = {'members': members}
        return self._do_json_call( \
                "a/groups/%s/members.add" % self.quote_id(group), \
                method='POST', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def delete_group_members(self, group, members):
        """
        POST /groups/{group-id}/members.delete
        members: A list of account ids that identify the accounts that
                should be added or deleted.
        TODO:
        _one_member: The id of one account that should be added or deleted.
          Content-Type: application/json;charset=UTF-8

          {
            "members": {
              "jane.roe@example.com",
              "john.doe@example.com"
            }
          }
        """
        if not group or not members:
            return None
        info = {'members': members}
        return self._do_json_call( \
                "a/groups/%s/members.delete" % self.quote_id(group), \
                method='POST', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def get_group_group(self, group, included_group):
        """
        GET /groups/{group-id}/groups/{group-id}
        """
        if not group or not included_group:
            return None
        return self._do_json_call("a/groups/%s/groups/%s" % \
                                    (self.quote_id(group), included_group))

    def add_group_group(self, group, included_group):
        """
        PUT /groups/{group-id}/groups/{group-id}
        """
        if not group or not included_group:
            return None
        return self._do_json_call("a/groups/%s/groups/%s" % \
                (self.quote_id(group), included_group), method='PUT', data="")

    def delete_group_group(self, group, included_group):
        """
        DELETE /groups/{group-id}/groups/{group-id}
        """
        if not group or not included_group:
            return None
        return self._do_json_call("a/groups/%s/groups/%s" % \
                    (self.quote_id(group), included_group), method='DELETE')

    def get_group_groups(self, group):
        """
        GET /groups/{group-id}/groups/
        """
        if not group:
            return None
        return self._do_json_call("a/groups/%s/groups" % self.quote_id(group))

    def add_group_groups(self, group, included_groups):
        """
        POST /groups/{group-id}/groups
        OR
        POST /groups/{group-id}/groups.add
        groups: A list of group ids that identify the groups that should be
                included or deleted.
        TODO:
        _one_group: The id of one group that should be included or deleted.
          Content-Type: application/json;charset=UTF-8

          {
            "groups": {
              "MyGroup",
              "MyOtherGroup"
            }
          }

        """
        if not group or not included_groups:
            return None
        info = {'groups': included_groups}
        return self._do_json_call( \
                "a/groups/%s/groups.add" % self.quote_id(group), \
                method='POST', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))


    def delete_group_groups(self, group, included_groups):
        """
        POST /groups/{group-id}/groups.delete
        groups: A list of group ids that identify the groups that should be
                included or deleted.
        TODO:
        _one_group The id of one group that should be included or deleted.
         Content-Type: application/json;charset=UTF-8

          {
            "members": {
              "MyGroup",
              "MyOtherGroup"
            }
          }
        """
        if not group or not included_groups:
            return None
        info = {'groups': included_groups}
        return self._do_json_call( \
                "a/groups/%s/groups.delete" % self.quote_id(group), \
                method='POST', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def get_changes(self, query=None, n=None, labels=False,
                    detailed_labels=False, current_revision=False,
                    all_revisions=False, current_commit=False,
                    all_commits=False, current_files=False, all_files=False,
                    detailed_accounts=False):
        """
        GET /changes/
        q = query
        n = number (int)
        """
        opts = []
        if query:
            opts.append("q=%s" % query)
        if n:
            opts.append("n=%d" % n)
        if all_files:
            opts.append("o=ALL_FILES")
            current_revision = True
        if current_commit:
            opts.append("o=CURRENT_COMMIT")
            if not current_revision or not all_revisions:
                current_revision = True
        if current_files:
            opts.append("o=CURRENT_FILES")
            if not current_revision or not all_revisions:
                current_revision = True
        if labels:
            opts.append("o=LABELS")
        if detailed_labels:
            opts.append("o=DETAILED_LABELS")
        if detailed_accounts:
            opts.append("o=DETAILED_ACCOUNTS")
        if current_revision:
            opts.append("o=CURRENT_REVISION")
        if all_revisions:
            opts.append("o=ALL_REVISIONS")
        if all_commits:
            opts.append("o=ALL_COMMITS")
        if opts:
            return self._do_json_call("a/changes/?%s" % "&".join(opts))
        else:
            return self._do_json_call("a/changes/")

    def get_change(self, change, project=None, branch=None):
        """
        GET /changes/{change-id}
        """
        if not change:
            return None
        return self._do_json_call("a/changes/%s" % \
                                    self.change_id(change, project, branch))

    def get_change_detail(self, change):
        """
        GET /changes/{change-id}/detail
        """
        if not change:
            return None
        return self._do_json_call("a/changes/%s/detail" % change)

    def get_change_topic(self, change):
        """
        GET /changes/{change-id}/topic
        """
        if not change:
            return None
        return self._do_json_call("a/changes/%s/topic" % change)

    def set_change_topic(self, change, topic=None, message=None):
        """
        PUT /changes/{change-id}/topic
        topic: The topic. The topic will be deleted if not set.
        message: Message to be added as review comment to the change when
                setting the topic.
        """
        if not change:
            return None
        info = {}
        if topic:
            info['topic'] = topic
        if message:
            info['message'] = message
        return self._do_json_call( \
                "a/changes/%s/topic" % change, \
                method='PUT', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def delete_change_topic(self, change):
        """
        DELETE /changes/{change-id}/topic
        """
        if not change:
            return None
        return self._do_json_call("a/changes/%s/topic" % \
                                    change, method='DELETE')

    def abandon_change(self, change, message=None):
        """
        POST /changes/{change-id}/abandon
        message: Message to be added as review comment to the change when
                abandoning the change.
        TODO: error handling (409 + message in body)
        """
        if not change:
            return None
        info = {}
        if message:
            info['message'] = message
        return self._do_json_call( \
                "a/changes/%s/abandon" % change, \
                method='POST', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def restore_change(self, change, message=None):
        """
        POST /changes/{change-id}/restore
        message: Message to be added as review comment to the change when
                restoring the change.
        TODO: error handling (409 + message in body)
        """
        if not change:
            return None
        info = {}
        if message:
            info['message'] = message
        return self._do_json_call( \
                "a/changes/%s/restore" % change, \
                method='POST', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def revert_change(self, change, message=None):
        """
        POST /changes/{change-id}/revert
        message: Message to be added as review comment to the change when
                reverting the change.
        TODO: error handling (409 + message in body)
        """
        if not change:
            return None
        info = {}
        if message:
            info['message'] = message
        return self._do_json_call( \
                "a/changes/%s/revert" % change, \
                method='POST', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def submit_change(self, change, wait_for_merge=None):
        """
        POST /changes/{change-id}/submit
        wait_for_merge: false if not set. Whether the request should wait for
                        the merge to complete.
        If false the request returns immediately after the change has been added
        to the merge queue and the caller can’t know whether the change could
        be merged successfully.
        TODO: error handling (409 + message in body)
        """
        if not change:
            return None
        info = {}
        if wait_for_merge is not None:
            info['wait_for_merge'] = wait_for_merge
        return self._do_json_call( \
                "a/changes/%s/submit" % change, \
                method='POST', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def get_change_reviewers(self, change):
        """
        GET /changes/{change-id}/reviewers/
        """
        if not change:
            return None
        return self._do_json_call("a/changes/%s/reviewers/" % change)

    def get_change_reviewer(self, change, reviewer):
        """
        GET /changes/{change-id}/reviewers/{account-id}
        """
        if not change:
            return None
        return self._do_json_call("a/changes/%s/reviewers/%s" % \
                                    (change, reviewer))

    def add_change_reviewer(self, change, reviewer, confirmed=None):
        """
        POST /changes/{change-id}/reviewers
        reviewer: The ID of one account that should be added as reviewer or
        the ID of one group for which all members should be added as reviewers.
        If an ID identifies both an account and a group, only the account is
        added as reviewer to the change.
        confirmed: Whether adding the reviewer is confirmed. The Gerrit server
            may be configured to require a confirmation when adding a group as
            reviewer that has many members.
        """
        if not change or not reviewer:
            return None
        info = {}
        if reviewer:
            info['reviewer'] = reviewer
        if confirmed is not None:
            info['confirmed'] = confirmed
        return self._do_json_call( \
                "a/changes/%s/reviewers" % change, \
                method='POST', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def delete_change_reviewer(self, change, reviewer):
        """
        DELETE /changes/{change-id}/reviewers/{account-id}
        """
        if not change or not reviewer:
            return None
        return self._do_json_call("a/changes/%s/reviewers/%s" % \
                                    (change, reviewer), method='DELETE')

    def get_change_review(self, change, revision):
        """
        GET /changes/{change-id}/revisions/{revision-id}/review
        """
        if not change or not revision:
            return None
        return self._do_json_call("a/changes/%s/revisions/%s/review" % \
                                                            (change, revision))

    def set_change_review(self, change, revision):
        """
        POST /changes/{change-id}/revisions/{revision-id}/review
        message: optional The message to be added as review comment.
        labels: optional The votes that should be added to the revision as a map that maps the label names to the voting values.
        comments: optional The comments that should be added as a map that maps a file path to a list of CommentInput entities.
        strict_labels: true if not set Whether all labels are required to be within the user’s permitted ranges based on access controls.
                                    If true, attempting to use a label not granted to the user will fail the entire modify operation early.
                                    If false, the operation will execute anyway, but the proposed labels will be modified to be the "best" value allowed by the access controls.
        drafts: optional Draft handling that defines how draft comments are handled that are already in the database but that were not also described in this input. Allowed values are DELETE, PUBLISH and KEEP. If not set, the default is DELETE.
        notify: optional Notify handling that defines to whom email notifications should be sent after the review is stored. Allowed values are NONE, OWNER, OWNER_REVIEWERS and ALL. If not set, the default is ALL.
        """
        raise NotImplementedError

    def submit_change_revision(self, change, revision, wait_for_merge=None):
        """
        POST /changes/{change-id}/revisions/{revision-id}/submit
        wait_for_merge: false if not set. Whether the request should wait for
                    the merge to complete.
                    If false the request returns immediately after the change
                    has been added to the merge queue and the caller can’t know
                    whether the change could be merged successfully.
        TODO: error handling (409 + message in body)
        """
        if not change or not revision:
            return None
        info = {}
        if wait_for_merge is not None:
            info['wait_for_merge'] = wait_for_merge
        return self._do_json_call( \
                "a/changes/%s/revisions/%s/submit" % (change, revision), \
                method='POST', \
                headers={'Content-Type': 'application/json;charset=UTF-8'}, \
                data=json.dumps(info))

    def get_change_revision_submit_type(self, change, revision):
        """
        GET /changes/{change-id}/revisions/{revision-id}/submit_type
        """
        if not change or not revision:
            return None
        return self._do_json_call("a/changes/%s/revisions/%s/submit_type" % \
                                                            (change, revision))


    def get_account(self, account="self"):
        """
        GET /accounts/{account-id}
        """
        return self._do_json_call("a/accounts/%s" % urllib2.quote(account))

    def get_account_capabilities(self, account="self"):
        """
        GET /accounts/{account-id}/capabilities
        """
        return self._do_json_call("a/accounts/%s/capabilities" % \
                                                    urllib2.quote(account, ""))

    def get_account_capability(self, account="self", capability=None):
        """
        GET /accounts/{account-id}/capabilities/{capability-id}
        returns text "ok" / 404 "not found raised"
        """
        if not capability:
            return None
        ret = self._do_http_call("a/accounts/%s/capabilities/%s" % \
                                    (urllib2.quote(account, ""), capability))
        if ret[0] and ret[0] == "ok\n":
            return True
        if ret[1] == 404:
            return False
        return None

    def get_account_groups(self, account="self"):
        """
        GET /accounts/{account-id}/groups/
        """
        return self._do_json_call("a/accounts/%s/groups" % \
                                    urllib2.quote(account, ""))

    def get_account_avatar(self, account="self", size=None):
        """
        GET /accounts/{account-id}/avatar
        HTTP/1.1 302 Found
        The response redirects to the URL of the avatar image.
        """
        raise NotImplementedError
