'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

import os
import sys
from .spinner import Spinner

ABORT = -5
CRITICAL = -4
ERROR = -3
WARNING = -2
LOG = -1
INFO = 1
CUSTOM = 1
QUIET = 0
VERBOSE = VERBOSE1 = 2
VERBOSE2 = 3
VERBOSE3 = 4
DEBUG = 5

PURPLE = "\033[95m"
YELLOW = "\033[93m"
RED = "\033[91m"
DARKRED = "\033[31m"
CYAN = "\033[36m"

class SRegistryMessage:

    def __init__(self, MESSAGELEVEL=None):
        self.level = get_logging_level()
        self.history = []
        self.errorStream = sys.stderr
        self.outputStream = sys.stdout
        self.colorize = self.useColor()
        self.colors = {ABORT: DARKRED,
                       CRITICAL: RED,
                       ERROR: RED,
                       WARNING: YELLOW,
                       LOG: PURPLE,
                       CUSTOM: PURPLE,
                       DEBUG: CYAN,
                       'OFF': "\033[0m", # end sequence
                       'CYAN':CYAN,
                       'PURPLE':PURPLE,
                       'RED':RED,
                       'DARKRED':DARKRED,
                       'YELLOW':YELLOW}

    # Colors --------------------------------------------

    def useColor(self):
        '''useColor will determine if color should be added
        to a print. Will check if being run in a terminal, and
        if has support for asci'''
        COLORIZE = get_user_color_preference()
        if COLORIZE is not None:
            return COLORIZE
        streams = [self.errorStream, self.outputStream]
        for stream in streams:
            if not hasattr(stream, 'isatty'):
                return False
            if not stream.isatty():
                return False
        return True

    def addColor(self, level, text):
        '''addColor to the prompt (usually prefix) if terminal
        supports, and specified to do so'''
        if self.colorize:
            if level in self.colors:
                text = "%s%s%s" % (self.colors[level],
                                   text,
                                   self.colors["OFF"])
        return text

    def emitError(self, level):
        '''determine if a level should print to
        stderr, includes all levels but INFO and QUIET'''
        if level in [ABORT,
                     ERROR,
                     WARNING,
                     VERBOSE,
                     VERBOSE1,
                     VERBOSE2,
                     VERBOSE3,
                     DEBUG]:
            return True
        return False

    def emitOutput(self, level):
        '''determine if a level should print to stdout
        only includes INFO'''
        if level in [LOG,
                     INFO]:
            return True
        return False

    def isEnabledFor(self, messageLevel):
        '''check if a messageLevel is enabled to emit a level
        '''
        if messageLevel <= self.level:
            return True
        return False

    def emit(self, level, message, prefix=None, color=None):
        '''emit is the main function to print the message
        optionally with a prefix
        :param level: the level of the message
        :param message: the message to print
        :param prefix: a prefix for the message
        '''
        if color is None:
            color = level

        if prefix is not None:
            prefix = self.addColor(color, "%s " % (prefix))
        else:
            prefix = ""
            message = self.addColor(color, message)

        # Add the prefix
        message = "%s%s" % (prefix, message)

        if not message.endswith('\n'):
            message = "%s\n" % message

        # If the level is quiet, only print to error
        if self.level == QUIET:
            pass

        # Otherwise if in range print to stdout and stderr
        elif self.isEnabledFor(level):
            if self.emitError(level):
                self.write(self.errorStream, message)
            else:
                self.write(self.outputStream, message)

        # Add all log messages to history
        self.history.append(message)

    def write(self, stream, message):
        '''write will write a message to a stream,
        first checking the encoding
        '''
        if isinstance(message, bytes):
            message = message.decode('utf-8')
        stream.write(message)

    def get_logs(self, join_newline=True):
        ''''get_logs will return the complete history, joined by newline
        (default) or as is.
        '''
        if join_newline:
            return '\n'.join(self.history)
        return self.history

    def show_progress(self,
                      iteration,
                      total,
                      length=40,
                      min_level=0,
                      prefix=None,
                      carriage_return=True,
                      suffix=None,
                      symbol=None):
        '''create a terminal progress bar, default bar shows for verbose+

           Parameters
           ==========
           iteration: current iteration (Int)
           total: total iterations (Int)
           length: character length of bar (Int)
        '''
        if not self.level == QUIET:

            percent = 100 * (iteration / float(total))
            progress = int(length * iteration // total)

            if suffix is None:
                suffix = ''

            if prefix is None:
                prefix = 'Progress'

            # Download sizes can be imperfect, setting carriage_return to False
            # and writing newline with caller cleans up the UI
            if percent >= 100:
                percent = 100
                progress = length

            if symbol is None:
                symbol = "="

            if progress < length:
                bar = symbol * progress + '|' + '-' * (length - progress - 1)
            else:
                bar = symbol * progress + '-' * (length - progress)

            # Only show progress bar for level > min_level
            if self.level > min_level:
                percent = "%5s" % ("{0:.1f}").format(percent)
                output = '\r' + prefix + \
                    " |%s| %s%s %s" % (bar, percent, '%', suffix)
                sys.stdout.write(output),
                if iteration == total and carriage_return:
                    sys.stdout.write('\n')
                sys.stdout.flush()

    # Logging ------------------------------------------

    def abort(self, message):
        self.emit(ABORT, message, 'ABORT')

    def critical(self, message):
        self.emit(CRITICAL, message, 'CRITICAL')

    def error(self, message):
        self.emit(ERROR, message, 'ERROR')

    def exit(self, message, return_code=1):
        self.emit(ERROR, message, 'ERROR')
        sys.exit(return_code)

    def warning(self, message):
        self.emit(WARNING, message, 'WARNING')

    def log(self, message):
        self.emit(LOG, message, 'LOG')

    def custom(self, prefix, message="", color=PURPLE):
        self.emit(CUSTOM, message, prefix, color)

    def info(self, message):
        self.emit(INFO, message)

    def newline(self):
        return self.info("")

    def verbose(self, message):
        self.emit(VERBOSE, message, "VERBOSE")

    def verbose1(self, message):
        self.emit(VERBOSE, message, "VERBOSE1")

    def verbose2(self, message):
        self.emit(VERBOSE2, message, 'VERBOSE2')

    def verbose3(self, message):
        self.emit(VERBOSE3, message, 'VERBOSE3')

    def debug(self, message):
        self.emit(DEBUG, message, 'DEBUG')

    def is_quiet(self):
        '''is_quiet returns true if the level is under 1
        '''
        if self.level < 1:
            return False
        return True

    # Terminal ------------------------------------------

    def table(self, rows, col_width=2):
        '''table will print a table of entries. If the rows is
        a dictionary, the keys are interpreted as column names. if
        not, a numbered list is used.
        '''

        labels = [str(x) for x in range(1,len(rows)+1)]
        if isinstance(rows, dict):
            labels = list(rows.keys())
            rows = list(rows.values())

        for row in rows:
            label = labels.pop(0)
            label = label.ljust(col_width)
            message = "\t".join(row)
            self.custom(prefix=label,
                        message=message)

def get_logging_level():
    '''get_logging_level will configure a logging to standard out based on the user's
    selected level, which should be in an environment variable called
    MESSAGELEVEL. if MESSAGELEVEL is not set, the maximum level
    (5) is assumed (all messages).
    '''
    level = os.environ.get("MESSAGELEVEL", INFO)

    # User knows logging levels and set one
    if isinstance(level, int):
        return level

    # Otherwise it's a string
    if level == "CRITICAL":
        return CRITICAL
    elif level == "ABORT":
        return ABORT
    elif level == "ERROR":
        return ERROR
    elif level == "WARNING":
        return WARNING
    elif level == "LOG":
        return LOG
    elif level == "INFO":
        return INFO
    elif level == "QUIET":
        return QUIET
    elif level.startswith("VERBOSE"):
        return VERBOSE3
    elif level == "LOG":
        return LOG
    elif level == "DEBUG":
        return DEBUG

    return level

def get_user_color_preference():
    COLORIZE = os.environ.get('SINGULARITY_COLORIZE', None)
    if COLORIZE is not None:
        COLORIZE = convert2boolean(COLORIZE)
    return COLORIZE

def convert2boolean(arg):
    '''convert2boolean is used for environmental variables that must be
    returned as boolean'''
    if not isinstance(arg, bool):
        return arg.lower() in ("yes", "true", "t", "1", "y")
    return arg

SRegistryMessage.spinner = Spinner()
bot = SRegistryMessage()


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.utils import (
    parse_image_name,
    remove_uri
)

# see the registry push for example of how to do this
from requests_toolbelt import (
    MultipartEncoder,
    MultipartEncoderMonitor
)

import requests
import sys
import os

def push(self, path, name, tag=None):
    '''push an image to Singularity Registry

    path: should correspond to an absolte image path (or derive it)
    name: should be the complete uri that the user has requested to push.
    tag: should correspond with an image tag. This is provided to mirror Docker
    '''
    path = os.path.abspath(path)
    bot.debug("PUSH %s" % path)

    if not os.path.exists(path):
        bot.error('%s does not exist.' %path)
        sys.exit(1)

    # This returns a data structure with collection, container, based on uri
    names = parse_image_name(remove_uri(name),tag=tag)

    # use Singularity client, if exists, to inspect to extract metadata
    metadata = self.get_metadata(path, names=names)

    # If you want a spinner
    bot.spinner.start()
    # do your push request here. Generally you want to except a KeyboardInterrupt
    # and give the user a status from the response
    bot.spinner.stop()
    # print('\n[Return status {0} {1}]'.format(response.status_code, message))

    # (see registry push for examople of creating progress bar)


'''

push.py: push functions for sregistry client

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from spython.main import Client as Singularity
from sregistry.logger import bot
from sregistry.defaults import SREGISTRY_STORAGE
from sregistry.utils import parse_image_name
import globus_sdk
from globus_sdk.exc import TransferAPIError
import json
import sys
import os

def push(self, path, name, tag=None):
    '''push an image to Globus endpoint. In this case, the name is the
       globus endpoint id and path.

       --name <endpointid>:/path/for/image

    '''

    # Split the name into endpoint and rest

    endpoint, remote = self._parse_endpoint_name(name)

    path = os.path.abspath(path)
    image = os.path.basename(path)
    bot.debug("PUSH %s" % path)

    # Flatten image uri into image name

    q = parse_image_name(image)

    if not os.path.exists(path):
        bot.error('%s does not exist.' %path)
        sys.exit(1)

    # Ensure we have a transfer client
    if not hasattr(self, 'transfer_client'):
        self._init_transfer_client()

    # The user must have a personal endpoint

    endpoints = self._get_endpoints()

    if len(endpoints['my-endpoints']) == 0:
        bot.error('You must have a personal endpoint to transfer the container')
        sys.exit(1)

    # Take the first endpoint that is active

    source_endpoint = None
    for eid,contender in endpoints['my-endpoints'].items():
       if contender['gcp_connected'] is True:
           source_endpoint = contender
           break

    # Exit if none are active, required!

    if source_endpoint is None:
        bot.error('No activated local endpoints online! Go online to transfer')
        sys.exit(1)

    # The destination endpoint should have an .singularity/shub folder set
    self._create_endpoint_cache(endpoint)

    # SREGISTRY_STORAGE must be an endpoint
    # if the image isn't already there, add it first

    added = self.add(image_path=path,
                     image_uri=q['uri'],
                     copy=True)

    label = "Singularity Registry Transfer for %s" %added.name
    tdata = globus_sdk.TransferData(self.transfer_client,
                                    source_endpoint['id'],
                                    endpoint,
                                    label=label,
                                    sync_level="checksum")
    image = ".singularity/shub/%s" %image
    tdata.add_item(added.image, image)
    bot.info('Requesting transfer from local %s to %s:%s' %(SREGISTRY_STORAGE,
                                                            endpoint, image))
    transfer_result = self.transfer_client.submit_transfer(tdata)
    bot.info(transfer_result['message'])
    return transfer_result


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot

def get_template(name):
    '''return a default template for some function in sregistry
       If there is no template, None is returned.

       Parameters
       ==========
       name: the name of the template to retrieve

    '''
    name = name.lower()
    templates = dict()

    templates['tarinfo'] = {"gid": 0,
                            "uid": 0,
                            "uname": "root",
                            "gname": "root",
                            "mode": 493}

    if name in templates:
        bot.debug("Found template for %s" % (name))
        return templates[name]
    else:
        bot.warning("Cannot find template %s" % (name))


'''

Copyright (C) 2018-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.defaults import SINGULARITY_CACHE
from sregistry.logger import bot
from sregistry.utils import ( mkdir_p, print_json, get_template, create_tar )
import json
import math
import os
import re
import requests
import shutil
import sys
import tempfile
from urllib.parse import urlparse

###############################################################################

def update_token(self):
    '''update_token uses HTTP basic authentication to get a token for
    Docker registry API V2 operations. We get here if a 401 is
    returned for a request.

    Parameters
    ==========
    response: the http request response to parse for the challenge.

    https://docs.docker.com/registry/spec/auth/token/
    '''
    # Add Amazon headers

    tokens = self.aws.get_authorization_token()
    token = tokens['authorizationData'][0]['authorizationToken']

    try:
        token = {"Authorization": "Basic %s" % token}
        self.headers.update(token)

    except Exception:
        bot.error("Error getting token.")
        sys.exit(1)

def download_layers(self, repo_name, digest=None, destination=None):
    ''' download layers is a wrapper to do the following for a client loaded
        with a manifest for an image:

        1. use the manifests to retrieve list of digests (get_digests)
        2. atomically download the list to destination (get_layers)

        This function uses the MultiProcess client to download layers
        at the same time.
    '''
    from sregistry.main.workers import Workers
    from sregistry.main.workers.aws import download_task

    # Obtain list of digets, and destination for download
    self._get_manifest(repo_name, digest)
    digests = self._get_digests(repo_name, digest)
    destination = self._get_download_cache(destination)

    # Create multiprocess download client
    workers = Workers()

    # Download each layer atomically
    tasks = []
    layers = []

    # Start with a fresh token
    self._update_token()

    for digest in digests:

        targz = "%s/%s.tar.gz" % (destination, digest['digest'])
        url = '%s/%s/blobs/%s' % (self.base, repo_name, digest['digest'])

        # Only download if not in cache already
        if not os.path.exists(targz):
            tasks.append((url, self.headers, targz))
        layers.append(targz)

    # Download layers with multiprocess workers
    if len(tasks) > 0:

        download_layers = workers.run(func=download_task,
                                      tasks=tasks)

    return layers, url

def get_manifest(self, repo_name, tag):
    '''return the image manifest via the aws client, saved in self.manifest
    '''

    image = None
    repo = self.aws.describe_images(repositoryName=repo_name)
    if 'imageDetails' in repo:
        for contender in repo.get('imageDetails'):
            if tag in contender['imageTags']:
                image = contender
                break

    # if the image isn't found, we need to exit
    if image is None:
        bot.exit('Cannot find %s:%s, is the uri correct?' %(repo_name, digest))

    digest = image['imageDigest']
    digests = self.aws.batch_get_image(repositoryName=repo_name,
                                       imageIds=[{"imageDigest": digest,
                                                  "imageTag": tag}])

    self.manifest = json.loads(digests['images'][0]['imageManifest'])
    return self.manifest

def get_digests(self, repo_name, tag):
    '''
       return a list of layers from a manifest.
       The function is intended to work with both version
       1 and 2 of the schema. All layers (including redundant)
       are returned. By default, we try version 2 first,
       then fall back to version 1.

       For version 1 manifests: extraction is reversed

       Parameters
       ==========
       manifest: the manifest to read_layers from

    '''
    if not hasattr(self, 'manifest'):
        bot.error('Please retrieve manifest for the image first.')
        sys.exit(1)

    # version 2 manifest here!
    return self.manifest['layers']


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.utils import ( read_file, get_installdir )
import sys
import os

def prepare_metadata(metadata):
    '''prepare a key/value list of metadata for the request. The metadata
       object that comes in is only parsed one level.
    '''
    pairs = {
        'metadata': {
            'items': [{
                'key': 'client',
                'value': 'sregistry'
            }
           ]
        }
    }
    for key,val in metadata.items():
        if not isinstance(val,dict) and not isinstance(val,list):
            pairs['metadata']['items'].append({'key':key,'value':val})
        elif isinstance(val,dict):
            for k,v in val.items():
                if not isinstance(v,dict) and not isinstance(v,list):
                    pairs['metadata']['items'].append({'key':k,'value':v})

    return pairs

def get_build_template(name=None, manager='apt'):
    '''get a particular build template, by default we return templates
       that are based on package managers.

       Parameters
       ==========
       name: the full path of the template file to use.
       manager: the package manager to use in the template (yum or apt)

    '''
    base = get_installdir()
    if name is None:
        name = "%s/main/templates/build/singularity-builder-%s.sh" %(base,
                                                                     manager)

    if os.path.exists(name):
        bot.debug("Found template %s" %name)
        return ''.join(read_file(name))

    bot.warning("Template %s not found." %name)


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.utils import (
    mkdir_p,
    parse_image_name,
    remove_uri,
    run_command,
    which
)

from sregistry.logger import bot
from sregistry.auth import ( read_client_secrets, update_client_secrets )
import os
import json

# Metadata

def get_metadata(self, image_file, names={}):
    '''extract metadata using Singularity inspect, if the executable is found.
       If not, return a reasonable default (the parsed image name)

       Parameters
       ==========
       image_file: the full path to a Singularity image
       names: optional, an extracted or otherwise created dictionary of
              variables for the image, likely from utils.parse_image_name

    '''
    metadata = dict()

    # We can't return anything without image_file or names
    if image_file is not None:
        if not os.path.exists(image_file):
            bot.error('Cannot find %s.' %image_file)
            return names or metadata

    # The user provided a file, but no names
    if not names:
        names = parse_image_name(remove_uri(image_file))

    # Look for the Singularity Executable
    singularity = which('singularity')['message']

    # Inspect the image, or return names only
    if os.path.exists(singularity) and image_file is not None:
        from spython.main import Client as Singularity

        # We try and inspect, but not required (wont work within Docker)
        try:
            Singularity.quiet = True
            updates = Singularity.inspect(image=image_file)
        except:
            bot.warning('Inspect command not supported, metadata not included.')
            updates = None

        # Try loading the metadata
        if updates is not None:
            try:
                updates = json.loads(updates)
                metadata.update(updates)
            except:
                pass

    metadata.update(names)
    return metadata


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from spython.main import Client as Singularity
from sregistry.utils import (
    get_tmpdir,
    parse_image_name,
    remove_uri,
    extract_tar
)
import shutil
import os
import sys

def pull(self, images,
               file_name=None,
               save=True,
               force=False,
               base=None,
               **kwargs):

    '''pull an image from a docker hub. This is a (less than ideal) workaround
       that actually does the following:

       - creates a sandbox folder
       - adds docker layers, metadata folder, and custom metadata to it
       - converts to a squashfs image with build

    the docker manifests are stored with registry metadata.

    Parameters
    ==========
    images: refers to the uri given by the user to pull in the format
    <collection>/<namespace>. You should have an API that is able to
    retrieve a container based on parsing this uri.
    file_name: the user's requested name for the file. It can
               optionally be None if the user wants a default.
    save: if True, you should save the container to the database
          using self.add()
    base: the registry base, in case the client doesn't want to set in env.

    Returns
    =======
    finished: a single container path, or list of paths

    '''

    if not isinstance(images,list):
        images = [images]

    bot.debug('Execution of PULL for %s images' %len(images))

    # If used internally we want to return a list to the user.
    finished = []
    for image in images:

        # 0. Update the base in case we aren't working with default
        base = self._update_base(image)
        q = parse_image_name(remove_uri(image), base=base)

        image_file = self._pull(file_name=file_name,
                                save=save,
                                force=force,
                                names=q,
                                kwargs=kwargs)

        finished.append(image_file)

    if len(finished) == 1:
        finished = finished[0]
    return finished

def _pull(self,
          file_name,
          names,
          save=True,
          force=False,
          uri="docker://",
          **kwargs):

    '''pull an image from a docker hub. This is a (less than ideal) workaround
       that actually does the following:

       - creates a sandbox folder
       - adds docker layers, metadata folder, and custom metadata to it
       - converts to a squashfs image with build

    the docker manifests are stored with registry metadata.

    Parameters
    ==========
    images: refers to the uri given by the user to pull in the format
    <collection>/<namespace>. You should have an API that is able to
    retrieve a container based on parsing this uri.
    file_name: the user's requested name for the file. It can
               optionally be None if the user wants a default.
    save: if True, you should save the container to the database
          using self.add()

    Returns
    =======
    finished: a single container path, or list of paths
    '''

    # Use Singularity to build the image, based on user preference
    if file_name is None:
        file_name = self._get_storage_name(names)

    # Determine if the user already has the image
    if os.path.exists(file_name) and force is False:
        bot.error('Image exists! Remove first, or use --force to overwrite')
        sys.exit(1)

    digest = names['version'] or names['tag']

    # Build from sandbox, prefix with sandbox
    sandbox = get_tmpdir(prefix="sregistry-sandbox")

    # First effort, get image via Sregistry
    layers = self._download_layers(names['url'], digest)

    # This is the url where the manifests were obtained
    url = self._get_manifest_selfLink(names['url'], digest)

    # Add environment to the layers
    envtar = self._get_environment_tar()
    layers = [envtar] + layers

    # Create singularity image from an empty folder
    for layer in layers:
        bot.info('Exploding %s' %layer)
        result = extract_tar(layer, sandbox, handle_whiteout=True)
        if result['return_code'] != 0:
            bot.error(result['message'])
            sys.exit(1)

    sudo = kwargs.get('sudo', False)

    # Build from a sandbox (recipe) into the image_file (squashfs)
    image_file = Singularity.build(image=file_name,
                                   recipe=sandbox,
                                   sudo=sudo)

    # Fall back to using Singularity
    if image_file is None:
        bot.info('Downloading with native Singularity, please wait...')
        image = image.replace('docker://', uri)
        image_file = Singularity.pull(image, pull_folder=sandbox)

    # Save to local storage
    if save is True:

        # Did we get the manifests?
        manifests = {}
        if hasattr(self, 'manifests'):
            manifests = self.manifests

        container = self.add(image_path = image_file,
                             image_uri = names['uri'],
                             metadata = manifests,
                             url = url)

        # When the container is created, this is the path to the image
        image_file = container.image

    if os.path.exists(image_file):
        bot.debug('Retrieved image file %s' %image_file)
        bot.custom(prefix="Success!", message=image_file)

    # Clean up sandbox
    shutil.rmtree(sandbox)

    return image_file


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.auth import read_client_secrets
from sregistry.main import ApiConnection

# here you should import the functions from the files in this
# folder that you add to your client (at the bottom)
# from .pull import pull
# from .push import push
# from .query import search

class Client(ApiConnection):

    def __init__(self, secrets=None, base=None, **kwargs):

        # You probably want to think about where  your base is coming from!
        self.base = base
        self._update_secrets()
        self._update_headers()
        super(ApiConnection, self).__init__(**kwargs)

    def _speak(self):
        '''if you want to add an extra print (of a parameter, for example)
           for the user when the client initalizes, write it here, eg:
           bot.info('[setting] value')
        '''
        pass

    def _update_secrets(self):
        '''update secrets will take a secrets credential file
           either located at .sregistry or the environment variable
           SREGISTRY_CLIENT_SECRETS and update the current client
           secrets as well as the associated API base. This is where you
           should do any customization of the secrets flie, or using
           it to update your client, if needed.
        '''
        # Get a setting for client myclient and some variable name VAR.
        # returns None if not set
        setting = self._get_setting('SREGISTRY_MYCLIENT_VAR')

        # Get (and if found in environment (1) settings (2) update the variable
        # It will still return None if not set
        setting = self._get_and_update_setting('SREGISTRY_MYCLIENT_VAR')

        # If you have a setting that is required and not found, you should exit.

        # Here is how to read all client secrets
        self.secrets = read_client_secrets()

        # If you don't want to use the shared settings file, you have your own.
        # Here is how to get if the user has a cache for you enabled, this
        # returns a path (enabled) or None (disabled) that you should honor
        # You can use this as a file path or folder and for both cases, you
        # need to create the file or folder
        if self._credential_cache is not None:
            bot.info("credential cache set to %s" %self._credential_cache)

    def __str__(self):
        return type(self)

# Add your different functions imported at the top to the client here
# Client.pull = pull
# Client.push = push
# Client.search = search


from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals

__all__ = ["S3FS"]

import contextlib
from datetime import datetime
import io
import itertools
import os
from ssl import SSLError
import tempfile
import threading
import mimetypes
import json

import boto3
from botocore.exceptions import ClientError, EndpointConnectionError

import six
from six import text_type

from fs import ResourceType
from fs.base import FS
from fs.info import Info
from fs import errors
from fs.mode import Mode
from fs.subfs import SubFS
from fs.path import basename, dirname, forcedir, join, normpath, relpath
from fs.time import datetime_to_epoch

def _make_repr(class_name, *args, **kwargs):
    """
    Generate a repr string.

    Positional arguments should be the positional arguments used to
    construct the class. Keyword arguments should consist of tuples of
    the attribute value and default. If the value is the default, then
    it won't be rendered in the output.

    Here's an example::

        def __repr__(self):
            return make_repr('MyClass', 'foo', name=(self.name, None))

    The output of this would be something line ``MyClass('foo',
    name='Will')``.

    """
    arguments = [repr(arg) for arg in args]
    arguments.extend(
        "{}={!r}".format(name, value)
        for name, (value, default) in sorted(kwargs.items())
        if value != default
    )
    return "{}({})".format(class_name, ", ".join(arguments))

class S3File(io.IOBase):
    """Proxy for a S3 file."""

    @classmethod
    def factory(cls, filename, mode, on_close):
        """Create a S3File backed with a temporary file."""
        _temp_file = tempfile.TemporaryFile()
        proxy = cls(_temp_file, filename, mode, on_close=on_close)
        return proxy

    def __repr__(self):
        return _make_repr(
            self.__class__.__name__, self.__filename, text_type(self.__mode)
        )

    def __init__(self, f, filename, mode, on_close=None):
        self._f = f
        self.__filename = filename
        self.__mode = mode
        self._on_close = on_close

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    @property
    def raw(self):
        return self._f

    def close(self):
        if self._on_close is not None:
            self._on_close(self)

    @property
    def closed(self):
        return self._f.closed

    def fileno(self):
        return self._f.fileno()

    def flush(self):
        return self._f.flush()

    def isatty(self):
        return self._f.asatty()

    def readable(self):
        return self.__mode.reading

    def readline(self, limit=-1):
        return self._f.readline(limit)

    def readlines(self, hint=-1):
        if hint == -1:
            return self._f.readlines(hint)
        else:
            size = 0
            lines = []
            for line in iter(self._f.readline, b""):
                lines.append(line)
                size += len(line)
                if size > hint:
                    break
            return lines

    def seek(self, offset, whence=os.SEEK_SET):
        if whence not in (os.SEEK_CUR, os.SEEK_END, os.SEEK_SET):
            raise ValueError("invalid value for 'whence'")
        self._f.seek(offset, whence)
        return self._f.tell()

    def seekable(self):
        return True

    def tell(self):
        return self._f.tell()

    def writable(self):
        return self.__mode.writing

    def writelines(self, lines):
        return self._f.writelines(lines)

    def read(self, n=-1):
        if not self.__mode.reading:
            raise IOError("not open for reading")
        return self._f.read(n)

    def readall(self):
        return self._f.readall()

    def readinto(self, b):
        return self._f.readinto()

    def write(self, b):
        if not self.__mode.writing:
            raise IOError("not open for reading")
        self._f.write(b)
        return len(b)

    def truncate(self, size=None):
        if size is None:
            size = self._f.tell()
        self._f.truncate(size)
        return size

@contextlib.contextmanager
def s3errors(path):
    """Translate S3 errors to FSErrors."""
    try:
        yield
    except ClientError as error:
        _error = error.response.get("Error", {})
        error_code = _error.get("Code", None)
        response_meta = error.response.get("ResponseMetadata", {})
        http_status = response_meta.get("HTTPStatusCode", 200)
        error_msg = _error.get("Message", None)
        if error_code == "NoSuchBucket":
            raise errors.ResourceError(path, exc=error, msg=error_msg)
        if http_status == 404:
            raise errors.ResourceNotFound(path)
        elif http_status == 403:
            raise errors.PermissionDenied(path=path, msg=error_msg)
        else:
            raise errors.OperationFailed(path=path, exc=error)
    except SSLError as error:
        raise errors.OperationFailed(path, exc=error)
    except EndpointConnectionError as error:
        raise errors.RemoteConnectionError(path, exc=error, msg="{}".format(error))

@six.python_2_unicode_compatible
class S3FS(FS):
    """
    Construct an Amazon S3 filesystem for
    `PyFilesystem <https://pyfilesystem.org>`_

    :param str bucket_name: The S3 bucket name.
    :param str dir_path: The root directory within the S3 Bucket.
        Defaults to ``"/"``
    :param str aws_access_key_id: The access key, or ``None`` to read
        the key from standard configuration files.
    :param str aws_secret_access_key: The secret key, or ``None`` to
        read the key from standard configuration files.
    :param str endpoint_url: Alternative endpoint url (``None`` to use
        default).
    :param str aws_session_token:
    :param str region: Optional S3 region.
    :param str delimiter: The delimiter to separate folders, defaults to
        a forward slash.
    :param bool strict: When ``True`` (default) S3FS will follow the
        PyFilesystem specification exactly. Set to ``False`` to disable
        validation of destination paths which may speed up uploads /
        downloads.
    :param str cache_control: Sets the 'Cache-Control' header for uploads.
    :param str acl: Sets the Access Control List header for uploads.
    :param dict upload_args: A dictionary for additional upload arguments.
        See https://boto3.readthedocs.io/en/latest/reference/services/s3.html#S3.Object.put
        for details.
    :param dict download_args: Dictionary of extra arguments passed to
        the S3 client.

    """

    _meta = {
        "case_insensitive": False,
        "invalid_path_chars": "\0",
        "network": True,
        "read_only": False,
        "thread_safe": True,
        "unicode_paths": True,
        "virtual": False,
    }

    _object_attributes = [
        "accept_ranges",
        "cache_control",
        "content_disposition",
        "content_encoding",
        "content_language",
        "content_length",
        "content_type",
        "delete_marker",
        "e_tag",
        "expiration",
        "expires",
        "last_modified",
        "metadata",
        "missing_meta",
        "parts_count",
        "replication_status",
        "request_charged",
        "restore",
        "server_side_encryption",
        "sse_customer_algorithm",
        "sse_customer_key_md5",
        "ssekms_key_id",
        "storage_class",
        "version_id",
        "website_redirect_location",
    ]

    def __init__(
        self,
        bucket_name,
        dir_path="/",
        aws_access_key_id=None,
        aws_secret_access_key=None,
        aws_session_token=None,
        endpoint_url=None,
        region=None,
        delimiter="/",
        strict=True,
        cache_control=None,
        acl=None,
        upload_args=None,
        download_args=None,
    ):
        _creds = (aws_access_key_id, aws_secret_access_key)
        if any(_creds) and not all(_creds):
            raise ValueError(
                "aws_access_key_id and aws_secret_access_key "
                "must be set together if specified"
            )
        self._bucket_name = bucket_name
        self.dir_path = dir_path
        self._prefix = relpath(normpath(dir_path)).rstrip("/")
        self.aws_access_key_id = aws_access_key_id
        self.aws_secret_access_key = aws_secret_access_key
        self.aws_session_token = aws_session_token
        self.endpoint_url = endpoint_url
        self.region = region
        self.delimiter = delimiter
        self.strict = strict
        self._tlocal = threading.local()
        if cache_control or acl:
            upload_args = upload_args or {}
            if cache_control:
                upload_args["CacheControl"] = cache_control
            if acl:
                upload_args["ACL"] = acl
        self.upload_args = upload_args
        self.download_args = download_args
        super(S3FS, self).__init__()

    def __repr__(self):
        return _make_repr(
            self.__class__.__name__,
            self._bucket_name,
            dir_path=(self.dir_path, "/"),
            region=(self.region, None),
            delimiter=(self.delimiter, "/"),
        )

    def __str__(self):
        return "<s3fs '{}'>".format(join(self._bucket_name, relpath(self.dir_path)))

    def _path_to_key(self, path):
        """Converts an fs path to a s3 key."""
        _path = relpath(normpath(path))
        _key = (
            "{}/{}".format(self._prefix, _path).lstrip("/").replace("/", self.delimiter)
        )
        return _key

    def _path_to_dir_key(self, path):
        """Converts an fs path to a s3 key."""
        _path = relpath(normpath(path))
        _key = (
            forcedir("{}/{}".format(self._prefix, _path))
            .lstrip("/")
            .replace("/", self.delimiter)
        )
        return _key

    def _key_to_path(self, key):
        return key.replace(self.delimiter, "/")

    def _get_object(self, path, key):
        _key = key.rstrip(self.delimiter)
        try:
            with s3errors(path):
                obj = self.s3.Object(self._bucket_name, _key)
                obj.load()
        except errors.ResourceNotFound:
            with s3errors(path):
                obj = self.s3.Object(self._bucket_name, _key + self.delimiter)
                obj.load()
                return obj
        else:
            return obj

    def _get_upload_args(self, key):
        upload_args = self.upload_args.copy() if self.upload_args else {}
        if "ContentType" not in upload_args:
            mime_type, _encoding = mimetypes.guess_type(key)
            if six.PY2 and mime_type is not None:
                mime_type = mime_type.decode("utf-8", "replace")
            upload_args["ContentType"] = mime_type or "binary/octet-stream"
        return upload_args

    @property
    def s3(self):
        if not hasattr(self._tlocal, "s3"):
            self._tlocal.s3 = boto3.resource(
                "s3",
                region_name=self.region,
                aws_access_key_id=self.aws_access_key_id,
                aws_secret_access_key=self.aws_secret_access_key,
                aws_session_token=self.aws_session_token,
                endpoint_url=self.endpoint_url,
            )
        return self._tlocal.s3

    @property
    def client(self):
        if not hasattr(self._tlocal, "client"):
            self._tlocal.client = boto3.client(
                "s3",
                region_name=self.region,
                aws_access_key_id=self.aws_access_key_id,
                aws_secret_access_key=self.aws_secret_access_key,
                aws_session_token=self.aws_session_token,
                endpoint_url=self.endpoint_url,
            )
        return self._tlocal.client

    def _info_from_object(self, obj, namespaces):
        """Make an info dict from an s3 Object."""
        key = obj.key
        path = self._key_to_path(key)
        name = basename(path.rstrip("/"))
        is_dir = key.endswith(self.delimiter)
        info = {"basic": {"name": name, "is_dir": is_dir}}
        if "details" in namespaces:
            _type = int(ResourceType.directory if is_dir else ResourceType.file)
            info["details"] = {
                "accessed": None,
                "modified": datetime_to_epoch(obj.last_modified),
                "size": obj.content_length,
                "type": _type,
            }
        if "s3" in namespaces:
            s3info = info["s3"] = {}
            for name in self._object_attributes:
                value = getattr(obj, name, None)
                if isinstance(value, datetime):
                    value = datetime_to_epoch(value)
                s3info[name] = value
        if "urls" in namespaces:
            url = self.client.generate_presigned_url(
                ClientMethod="get_object",
                Params={"Bucket": self._bucket_name, "Key": key},
            )
            info["urls"] = {"download": url}
        return info

    def isdir(self, path):
        _path = self.validatepath(path)
        try:
            return self._getinfo(_path).is_dir
        except errors.ResourceNotFound:
            return False

    def getinfo(self, path, namespaces=None):
        self.check()
        namespaces = namespaces or ()
        _path = self.validatepath(path)
        _key = self._path_to_key(_path)

        try:
            dir_path = dirname(_path)
            if dir_path != "/":
                _dir_key = self._path_to_dir_key(dir_path)
                with s3errors(path):
                    obj = self.s3.Object(self._bucket_name, _dir_key)
                    obj.load()
        except errors.ResourceNotFound:
            raise errors.ResourceNotFound(path)

        if _path == "/":
            return Info(
                {
                    "basic": {"name": "", "is_dir": True},
                    "details": {"type": int(ResourceType.directory)},
                }
            )

        obj = self._get_object(path, _key)
        info = self._info_from_object(obj, namespaces)
        return Info(info)

    def _getinfo(self, path, namespaces=None):
        """Gets info without checking for parent dir."""
        namespaces = namespaces or ()
        _path = self.validatepath(path)
        _key = self._path_to_key(_path)
        if _path == "/":
            return Info(
                {
                    "basic": {"name": "", "is_dir": True},
                    "details": {"type": int(ResourceType.directory)},
                }
            )

        obj = self._get_object(path, _key)
        info = self._info_from_object(obj, namespaces)
        return Info(info)

    def listdir(self, path):
        _path = self.validatepath(path)
        _s3_key = self._path_to_dir_key(_path)
        prefix_len = len(_s3_key)

        paginator = self.client.get_paginator("list_objects")
        with s3errors(path):
            _paginate = paginator.paginate(
                Bucket=self._bucket_name, Prefix=_s3_key, Delimiter=self.delimiter
            )
            _directory = []
            for result in _paginate:
                common_prefixes = result.get("CommonPrefixes", ())
                for prefix in common_prefixes:
                    _prefix = prefix.get("Prefix")
                    _name = _prefix[prefix_len:]
                    if _name:
                        _directory.append(_name.rstrip(self.delimiter))
                for obj in result.get("Contents", ()):
                    name = obj["Key"][prefix_len:]
                    if name:
                        _directory.append(name)

        if not _directory:
            if not self.getinfo(_path).is_dir:
                raise errors.DirectoryExpected(path)

        return _directory

    def makedir(self, path, permissions=None, recreate=False):
        self.check()
        _path = self.validatepath(path)
        _key = self._path_to_dir_key(_path)

        if not self.isdir(dirname(_path)):
            raise errors.ResourceNotFound(path)

        try:
            self._getinfo(path)
        except errors.ResourceNotFound:
            pass
        else:
            if recreate:
                return self.opendir(_path)
            else:
                raise errors.DirectoryExists(path)
        with s3errors(path):
            _obj = self.s3.Object(self._bucket_name, _key)
            _obj.put(**self._get_upload_args(_key))
        return SubFS(self, path)

    def openbin(self, path, mode="r", buffering=-1, **options):
        _mode = Mode(mode)
        _mode.validate_bin()
        self.check()
        _path = self.validatepath(path)
        _key = self._path_to_key(_path)

        if _mode.create:

            def on_close_create(s3file):
                """Called when the S3 file closes, to upload data."""
                try:
                    s3file.raw.seek(0)
                    with s3errors(path):
                        self.client.upload_fileobj(
                            s3file.raw,
                            self._bucket_name,
                            _key,
                            ExtraArgs=self._get_upload_args(_key),
                        )
                finally:
                    s3file.raw.close()

            try:
                dir_path = dirname(_path)
                if dir_path != "/":
                    _dir_key = self._path_to_dir_key(dir_path)
                    self._get_object(dir_path, _dir_key)
            except errors.ResourceNotFound:
                raise errors.ResourceNotFound(path)

            try:
                info = self._getinfo(path)
            except errors.ResourceNotFound:
                pass
            else:
                if _mode.exclusive:
                    raise errors.FileExists(path)
                if info.is_dir:
                    raise errors.FileExpected(path)

            s3file = S3File.factory(path, _mode, on_close=on_close_create)
            if _mode.appending:
                try:
                    with s3errors(path):
                        self.client.download_fileobj(
                            self._bucket_name,
                            _key,
                            s3file.raw,
                            ExtraArgs=self.download_args,
                        )
                except errors.ResourceNotFound:
                    pass
                else:
                    s3file.seek(0, os.SEEK_END)

            return s3file

        if self.strict:
            info = self.getinfo(path)
            if info.is_dir:
                raise errors.FileExpected(path)

        def on_close(s3file):
            """Called when the S3 file closes, to upload the data."""
            try:
                if _mode.writing:
                    s3file.raw.seek(0, os.SEEK_SET)
                    with s3errors(path):
                        self.client.upload_fileobj(
                            s3file.raw,
                            self._bucket_name,
                            _key,
                            ExtraArgs=self._get_upload_args(_key),
                        )
            finally:
                s3file.raw.close()

        s3file = S3File.factory(path, _mode, on_close=on_close)
        with s3errors(path):
            self.client.download_fileobj(
                self._bucket_name, _key, s3file.raw, ExtraArgs=self.download_args
            )
        s3file.seek(0, os.SEEK_SET)
        return s3file

    def remove(self, path):
        self.check()
        _path = self.validatepath(path)
        _key = self._path_to_key(_path)
        if self.strict:
            info = self.getinfo(path)
            if info.is_dir:
                raise errors.FileExpected(path)
        self.client.delete_object(Bucket=self._bucket_name, Key=_key)

    def isempty(self, path):
        self.check()
        _path = self.validatepath(path)
        _key = self._path_to_dir_key(_path)
        response = self.client.list_objects(
            Bucket=self._bucket_name, Prefix=_key, MaxKeys=2
        )
        contents = response.get("Contents", ())
        for obj in contents:
            if obj["Key"] != _key:
                return False
        return True

    def removedir(self, path):
        self.check()
        _path = self.validatepath(path)
        if _path == "/":
            raise errors.RemoveRootError()
        info = self.getinfo(_path)
        if not info.is_dir:
            raise errors.DirectoryExpected(path)
        if not self.isempty(path):
            raise errors.DirectoryNotEmpty(path)
        _key = self._path_to_dir_key(_path)
        self.client.delete_object(Bucket=self._bucket_name, Key=_key)

    def setinfo(self, path, info):
        self.getinfo(path)

    def readbytes(self, path):
        self.check()
        if self.strict:
            info = self.getinfo(path)
            if not info.is_file:
                raise errors.FileExpected(path)
        _path = self.validatepath(path)
        _key = self._path_to_key(_path)
        bytes_file = io.BytesIO()
        with s3errors(path):
            self.client.download_fileobj(
                self._bucket_name, _key, bytes_file, ExtraArgs=self.download_args
            )
        return bytes_file.getvalue()

    def download(self, path, file, chunk_size=None, **options):
        self.check()
        if self.strict:
            info = self.getinfo(path)
            if not info.is_file:
                raise errors.FileExpected(path)
        _path = self.validatepath(path)
        _key = self._path_to_key(_path)
        with s3errors(path):
            self.client.download_fileobj(
                self._bucket_name, _key, file, ExtraArgs=self.download_args
            )

    def exists(self, path):
        self.check()
        _path = self.validatepath(path)
        if _path == "/":
            return True
        _key = self._path_to_dir_key(_path)
        try:
            self._get_object(path, _key)
        except errors.ResourceNotFound:
            return False
        else:
            return True

    def scandir(self, path, namespaces=None, page=None):
        _path = self.validatepath(path)
        namespaces = namespaces or ()
        _s3_key = self._path_to_dir_key(_path)
        prefix_len = len(_s3_key)

        info = self.getinfo(path)
        if not info.is_dir:
            raise errors.DirectoryExpected(path)

        paginator = self.client.get_paginator("list_objects")
        _paginate = paginator.paginate(
            Bucket=self._bucket_name, Prefix=_s3_key, Delimiter=self.delimiter
        )

        def gen_info():
            for result in _paginate:
                common_prefixes = result.get("CommonPrefixes", ())
                for prefix in common_prefixes:
                    _prefix = prefix.get("Prefix")
                    _name = _prefix[prefix_len:]
                    if _name:
                        info = {
                            "basic": {
                                "name": _name.rstrip(self.delimiter),
                                "is_dir": True,
                            }
                        }
                        yield Info(info)
                for _obj in result.get("Contents", ()):
                    name = _obj["Key"][prefix_len:]
                    if name:
                        with s3errors(path):
                            obj = self.s3.Object(self._bucket_name, _obj["Key"])
                        info = self._info_from_object(obj, namespaces)
                        yield Info(info)

        iter_info = iter(gen_info())
        if page is not None:
            start, end = page
            iter_info = itertools.islice(iter_info, start, end)

        for info in iter_info:
            yield info

    def writebytes(self, path, contents):
        if not isinstance(contents, bytes):
            raise TypeError("contents must be bytes")

        _path = self.validatepath(path)
        _key = self._path_to_key(_path)
        if self.strict:
            if not self.isdir(dirname(path)):
                raise errors.ResourceNotFound(path)
            try:
                info = self._getinfo(path)
                if info.is_dir:
                    raise errors.FileExpected(path)
            except errors.ResourceNotFound:
                pass

        bytes_file = io.BytesIO(contents)
        with s3errors(path):
            self.client.upload_fileobj(
                bytes_file,
                self._bucket_name,
                _key,
                ExtraArgs=self._get_upload_args(_key),
            )

    def upload(self, path, file, chunk_size=None, **options):
        _path = self.validatepath(path)
        _key = self._path_to_key(_path)

        if self.strict:
            if not self.isdir(dirname(path)):
                raise errors.ResourceNotFound(path)
            try:
                info = self._getinfo(path)
                if info.is_dir:
                    raise errors.FileExpected(path)
            except errors.ResourceNotFound:
                pass

        with s3errors(path):
            self.client.upload_fileobj(
                file, self._bucket_name, _key, ExtraArgs=self._get_upload_args(_key)
            )

    def copy(self, src_path, dst_path, overwrite=False):
        if not overwrite and self.exists(dst_path):
            raise errors.DestinationExists(dst_path)
        _src_path = self.validatepath(src_path)
        _dst_path = self.validatepath(dst_path)
        if self.strict:
            if not self.isdir(dirname(_dst_path)):
                raise errors.ResourceNotFound(dst_path)
        _src_key = self._path_to_key(_src_path)
        _dst_key = self._path_to_key(_dst_path)
        try:
            with s3errors(src_path):
                self.client.copy_object(
                    Bucket=self._bucket_name,
                    Key=_dst_key,
                    CopySource={"Bucket": self._bucket_name, "Key": _src_key},
                )
        except errors.ResourceNotFound:
            if self.exists(src_path):
                raise errors.FileExpected(src_path)
            raise

    def move(self, src_path, dst_path, overwrite=False):
        self.copy(src_path, dst_path, overwrite=overwrite)
        self.remove(src_path)

    def geturl(self, path, purpose="download"):
        _path = self.validatepath(path)
        _key = self._path_to_key(_path)
        if _path == "/":
            raise errors.NoURL(path, purpose)
        if purpose == "download":
            url = self.client.generate_presigned_url(
                ClientMethod="get_object",
                Params={"Bucket": self._bucket_name, "Key": _key},
            )
            return url
        else:
            raise errors.NoURL(path, purpose)


from django import template
from django.utils.html import escape
from django.utils.safestring import mark_safe

from ..helpers import GRAVATAR_DEFAULT_SIZE, get_gravatar_profile_url, get_gravatar_url

# Get template.Library instance
register = template.Library()

def gravatar_url(user_or_email, size=GRAVATAR_DEFAULT_SIZE):
    """ Builds a gravatar url from an user or email """
    if hasattr(user_or_email, 'email'):
        email = user_or_email.email
    else:
        email = user_or_email

    try:
        return escape(get_gravatar_url(email=email, size=size))
    except:
        return ''

def gravatar(user_or_email, size=GRAVATAR_DEFAULT_SIZE, alt_text='', css_class='gravatar'):
    """ Builds an gravatar <img> tag from an user or email """
    if hasattr(user_or_email, 'email'):
        email = user_or_email.email
    else:
        email = user_or_email

    try:
        url = escape(get_gravatar_url(email=email, size=size))
    except:
        return ''

    return mark_safe(
        '<img class="{css_class}" src="{src}" width="{width}"'
        ' height="{height}" alt="{alt}" />'.format(
            css_class=css_class, src=url, width=size, height=size, alt=alt_text
        )
    )

def gravatar_profile_url(user_or_email):
    if hasattr(user_or_email, 'email'):
        email = user_or_email.email
    else:
        email = user_or_email

    try:
        return get_gravatar_profile_url(email)
    except:
        return ''

register.simple_tag(gravatar_url)
register.simple_tag(gravatar)
register.simple_tag(gravatar_profile_url)


import hashlib

from django.conf import settings

from .compat import urlencode, urlopen, Request, HTTPError, URLError

# These options can be used to change the default image if no gravatar is found
GRAVATAR_DEFAULT_IMAGE_404 = '404'
GRAVATAR_DEFAULT_IMAGE_MYSTERY_MAN = 'mm'
GRAVATAR_DEFAULT_IMAGE_IDENTICON = 'identicon'
GRAVATAR_DEFAULT_IMAGE_MONSTER = 'monsterid'
GRAVATAR_DEFAULT_IMAGE_WAVATAR = 'wavatar'
GRAVATAR_DEFAULT_IMAGE_RETRO = 'retro'

# These options can be used to restrict gravatar content
GRAVATAR_RATING_G = 'g'
GRAVATAR_RATING_PG = 'pg'
GRAVATAR_RATING_R = 'r'
GRAVATAR_RATING_X = 'x'

# Get Gravatar base url from settings.py
GRAVATAR_URL = getattr(settings, 'GRAVATAR_URL', 'http://www.gravatar.com/')
GRAVATAR_SECURE_URL = getattr(settings, 'GRAVATAR_SECURE_URL', 'https://secure.gravatar.com/')

# Get user defaults from settings.py
GRAVATAR_DEFAULT_SIZE = getattr(settings, 'GRAVATAR_DEFAULT_SIZE', 80)
GRAVATAR_DEFAULT_IMAGE = getattr(settings, 'GRAVATAR_DEFAULT_IMAGE',
        GRAVATAR_DEFAULT_IMAGE_MYSTERY_MAN)
GRAVATAR_DEFAULT_RATING = getattr(settings, 'GRAVATAR_DEFAULT_RATING',
        GRAVATAR_RATING_G)
GRAVATAR_DEFAULT_SECURE = getattr(settings, 'GRAVATAR_DEFAULT_SECURE', True)

def calculate_gravatar_hash(email):
    # Calculate the email hash
    enc_email = email.strip().lower().encode("utf-8")
    email_hash = hashlib.md5(enc_email).hexdigest()
    return email_hash

def get_gravatar_url(email, size=GRAVATAR_DEFAULT_SIZE, default=GRAVATAR_DEFAULT_IMAGE,
        rating=GRAVATAR_DEFAULT_RATING, secure=GRAVATAR_DEFAULT_SECURE):
    """
    Builds a url to a gravatar from an email address.

    :param email: The email to fetch the gravatar for
    :param size: The size (in pixels) of the gravatar to fetch
    :param default: What type of default image to use if the gravatar does not exist
    :param rating: Used to filter the allowed gravatar ratings
    :param secure: If True use https, otherwise plain http
    """
    if secure:
        url_base = GRAVATAR_SECURE_URL
    else:
        url_base = GRAVATAR_URL

    # Calculate the email hash
    email_hash = calculate_gravatar_hash(email)

    # Build querystring
    query_string = urlencode({
        's': str(size),
        'd': default,
        'r': rating,
    })

    # Build url
    url = '{base}avatar/{hash}.jpg?{qs}'.format(base=url_base,
            hash=email_hash, qs=query_string)

    return url

def has_gravatar(email):
    """
    Returns True if the user has a gravatar, False if otherwise
    """
    # Request a 404 response if the gravatar does not exist
    url = get_gravatar_url(email, default=GRAVATAR_DEFAULT_IMAGE_404)

    # Verify an OK response was received
    try:
        request = Request(url)
        request.get_method = lambda: 'HEAD'
        return 200 == urlopen(request).code
    except (HTTPError, URLError):
        return False

def get_gravatar_profile_url(email, secure=GRAVATAR_DEFAULT_SECURE):
    """
    Builds a url to a gravatar profile from an email address.

    :param email: The email to fetch the gravatar for
    :param secure: If True use https, otherwise plain http
    """
    if secure:
        url_base = GRAVATAR_SECURE_URL
    else:
        url_base = GRAVATAR_URL

    # Calculate the email hash
    email_hash = calculate_gravatar_hash(email)

    # Build url
    url = '{base}{hash}'.format(base=url_base, hash=email_hash)

    return url


"""
This file contains a worked example of using minorminer to embed
a structured problem, where that structure can be exploited for
speed.  Additionally, we demonstrate the use of a few optional
parameters, including fixed chains.

The structured problem, 4-coloring, is to assign one of 4 colors
to each node in a graph, such that no two adjacent nodes have the
same color.  The QUBO structure for this problem is described in
`graph_coloring_qubo`.

The key observation is that the variables of this QUBO have the
form (v,c) where v is a node in the original graph and c is a
color; that edges ((v1,c1), (v2,c2)) have either v1=v2 or c1=c2.
If we take a quotient of this qubo, identifying variables which
have the same node label, we get the original graph back.

Next, we describe a quotient of a Chimera graph, implemented in
`chimera_blocks` and `chimera_block_quotient`.  The so-called
"chimera index" is a 4-tuple of the form (x,y,u,k) where
0 <= k < 4 and 0 <= u < 2.  We take a quotient of this graph,
identifying nodes which agree in the first three terms, (x,y,u).

After taking these quotients, our problem is simple.  We need
to embed the quotient QUBO into the block quotient of Chimera,
with one additional requirement: the chain for each QUBO node
must contain two blocks of the form (x1, y1, 0) and (x1, y1, 1).
The function `embed_with_quotient` performs the reduction and
supplies those additional requirements to `find_embedding`.

"""

# before we get to anything else, let's get some imports out of the way.
from __future__ import print_function
from past.builtins import xrange
import networkx as nx
import dwave_networkx as dnx
from minorminer import find_embedding
from time import clock

def graph_coloring_qubo(graph, k):
    """
    the QUBO for k-coloring a graph A is as follows:

    variables:
    x_{v,c} = 1 if vertex v of A gets color c; x_{v,c} = 0 otherwise

    constraints:
    1)  each v in A gets exactly one color.
       This constraint is enforced by including the term (\sum_c x_{v,c} - 1)^2 in the QUBO,
       which is minimized when \sum_c x_{v,c} = 1.

    2) If u and v in A are adjacent, then they get different colors.
       This constraint is enforced by including terms x_{v,c} x_{u,c} in the QUBO,
       which is minimzed when at most one of u and v get color c.

    Total QUBO:
    Q(x) = \sum_v (\sum_c x_{v,c} - 1)^2  + \sum_{u ~ v} \sum_c x_{v,c} x_{u,c}

    The graph of interactions for this QUBO consists of cliques of size k (with vertices {x_{v,c} for c = 0,...,k-1})
    plus k disjoint copies of the graph A (one for each color).

    """

    K = nx.complete_graph(k)
    g1 = nx.cartesian_product(nx.create_empty_copy(graph), K)
    g2 = nx.cartesian_product(graph, nx.create_empty_copy(K))
    return nx.compose(g1, g2)

def chimera_blocks(M=16, N=16, L=4):
    """
    Generator for blocks for a chimera block quotient
    """
    for x in xrange(M):
        for y in xrange(N):
            for u in (0, 1):
                yield tuple((x, y, u, k) for k in xrange(L))

def chimera_block_quotient(G, blocks):
    """
    Extract the blocks from a graph, and returns a
    block-quotient graph according to the acceptability
    functions block_good and eblock_good

    Inputs:
        G: a networkx graph
        blocks: a tuple of tuples

    """
    from networkx import Graph
    from itertools import product

    BG = Graph()
    blockid = {}
    for i, b in enumerate(blocks):
        BG.add_node(i)
        if not b or not all(G.has_node(x) for x in b):
            continue
        for q in b:
            if q in blockid:
                raise(RuntimeError, "two blocks overlap")
            blockid[q] = i

    for q, u in blockid.items():
        ublock = blocks[u]
        for p in G[q]:
            if p not in blockid:
                continue
            v = blockid[p]
            if BG.has_edge(u, v) or u == v:
                continue
            vblock = blocks[v]

            if ublock[0][2] == vblock[0][2]:
                block_edges = zip(ublock, vblock)
            else:
                block_edges = product(ublock, vblock)

            if all(G.has_edge(x, y) for x, y in block_edges):
                BG.add_edge(u, v)

    return BG

def embed_with_quotient(source_graph, target_graph, M=16, N=16, L=4, **args):
    """
    Produce an embedding in target_graph suitable to
    check if source_graph is 4-colorable.  More generally,
    if target_graph is a (M,N,L) Chimera subgraph, the
    test is for L-colorability.  This depends heavily upon
    the Chimera structure

    Inputs:
        source_graph, target_graph: networkx graphs
        M,N,L: integers defining the base chimera topology

    Outputs:
        emb: a dictionary mapping (v,i)

    """
    from random import sample
    blocks = list(chimera_blocks(M, N, L))

    BG = chimera_block_quotient(target_graph, blocks)

    ublocks = {block: (block[0][2], i)
               for (i, block) in enumerate(blocks) if BG.has_node(i)}
    source_e = list(source_graph.edges())
    source_n = {x for e in source_e for x in e}
    fabric_e = list(BG.edges())

    # Construct the hints:
    # Goal: each source node must be connected to one horizontal block and one
    #       vertical block (by Chimera structure, each source node will
    #       contain a full (horizontal and vertical) unit cell
    # Construction:
    #   0. for each source node `z`, construct two dummy nodes (z,0) and (z,1)
    #     in both the source and target graphs used by the embedder
    #   1. fix the embedding `(z,u) -> (z,u)` for all dummy nodes
    #   2. for each target block `i` with orientation `u`, and for each source
    #     node `z`, add the target edge `((z,u), i)`
    #   3. for each source node `z` and each orientation `u`, add the source
    #     edge `((z,u), z)`

    fix_chains = {}
    for z in source_n:
        for u in (0, 1):
            source_e.append(((z, u), z))
            fix_chains[z, u] = [(z, u)]
        for u, i in ublocks.values():
            fabric_e.append(((z, u), i))

    # first, grab a few embeddings in the quotient graph. this is super fast
    embs = filter(None, [find_embedding(source_e, fabric_e,
                                        fixed_chains=fix_chains,
                                        chainlength_patience=0,
                                        **args) for _ in range(10)])

    # select the best-looking candidate so far
    emb = min(embs, key=lambda e: sorted((len(c)
                                          for c in e.values()), reverse=True))

    # work down the chainlengths in our embeding
    for _ in range(10):
        emb = find_embedding(source_e, fabric_e,
                             fixed_chains=fix_chains,
                             initial_chains=emb,
                             chainlength_patience=3,
                             skip_initialization=True,
                             **args)

    # next, translate the block-embedding to a qubit-embedding
    newemb = {}
    for v in source_n:
        for k in range(L):
            newemb[v, k] = [blocks[i][k] for i in emb[v]]

    return newemb

if __name__ == "__main__":
    # first, we construct a Chimera graph
    G = dnx.chimera_graph(16)
    labs = nx.get_node_attributes(G, 'chimera_index')
    unlab = {d: i for i, d in labs.items()}
    H = nx.relabel_nodes(G, labs)

    # Now we take a graph to be colored
    graph = nx.generators.triangular_lattice_graph(8, 8)

    # for a basis of comparison, let's try to embed this without the quotient
    graph4 = graph_coloring_qubo(graph, 4)
    c = clock()
    emb = find_embedding(graph4.edges(), H.edges(),
                         verbose=0, chainlength_patience=30)
    try:
        print("raw embedding %d seconds, " % (clock() - c), end='')
        cl = max(len(c) for c in emb.values())
        print("maximum chainlength %d" % cl)
    except:
        print("failure")

    # we embed it using the block quotient,
    c = clock()
    emb = embed_with_quotient(graph, H, 16, 16, 4)
    # and then translate back to integer indices
    print("quotient embedding %d seconds, maximum chainlength %d" % (clock() - c, max(len(c) for c in emb.values())))

    # finally, we translate the embedding back to integer labels
    newemb = {v: [unlab[q] for q in c] for v, c in emb.items()}
    for v in graph:
        for k in range(4):
            print((v, k), newemb[v, k])


# -*- coding: utf-8 -*-
"""
molvs.resonance
~~~~~~~~~~~~~~~

Resonance (mesomeric) transformations.

"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import logging

from rdkit import Chem

log = logging.getLogger(__name__)

MAX_STRUCTURES = 1000

class ResonanceEnumerator(object):
    """Simple wrapper around RDKit ResonanceMolSupplier.

    """

    def __init__(self, kekule_all=False, allow_incomplete_octets=False, unconstrained_cations=False,
                 unconstrained_anions=False, allow_charge_separation=False, max_structures=MAX_STRUCTURES):
        """

        :param bool allow_incomplete_octets: include resonance structures whose octets are less complete than the the most octet-complete structure.
        :param bool allow_charge_separation: include resonance structures featuring charge separation also when uncharged resonance structures exist.
        :param bool kekule_all: enumerate all possible degenerate Kekule resonance structures (the default is to include just one).
        :param bool unconstrained_cations: if False positively charged atoms left and right of N with an incomplete octet are acceptable only if the conjugated group has a positive total formal charge.
        :param bool unconstrained_anions: if False, negatively charged atoms left of N are acceptable only if the conjugated group has a negative total formal charge.
        :param int max_structures: Maximum number of resonance forms.
        """
        self.kekule_all = kekule_all
        self.allow_incomplete_octets = allow_incomplete_octets
        self.unconstrained_cations = unconstrained_cations
        self.unconstrained_anions = unconstrained_anions
        self.allow_charge_separation = allow_charge_separation
        self.max_structures = max_structures

    def __call__(self, mol):
        """Calling a ResonanceEnumerator instance like a function is the same as calling its enumerate(mol) method."""
        return self.enumerate(mol)

    def enumerate(self, mol):
        """Enumerate all possible resonance forms and return them as a list.

        :param mol: The input molecule.
        :type mol: rdkit.Chem.rdchem.Mol
        :return: A list of all possible resonance forms of the molecule.
        :rtype: list of rdkit.Chem.rdchem.Mol
        """
        flags = 0
        if self.kekule_all:
            flags = flags | Chem.KEKULE_ALL
        if self.allow_incomplete_octets:
            flags = flags | Chem.ALLOW_INCOMPLETE_OCTETS
        if self.allow_charge_separation:
            flags = flags | Chem.ALLOW_CHARGE_SEPARATION
        if self.unconstrained_anions:
            flags = flags | Chem.UNCONSTRAINED_ANIONS
        if self.unconstrained_cations:
            flags = flags | Chem.UNCONSTRAINED_CATIONS
        results = []
        for result in Chem.ResonanceMolSupplier(mol, flags=flags, maxStructs=self.max_structures):
            # This seems necessary? ResonanceMolSupplier only does a partial sanitization
            Chem.SanitizeMol(result)
            results.append(result)
        return results

        # Potentially interesting: getNumConjGrps(), getBondConjGrpIdx() and getAtomConjGrpIdx()

def enumerate_resonance_smiles(smiles):
    """Return a set of resonance forms as SMILES strings, given a SMILES string.

    :param smiles: A SMILES string.
    :returns: A set containing SMILES strings for every possible resonance form.
    :rtype: set of strings.
    """
    mol = Chem.MolFromSmiles(smiles)
    #Chem.SanitizeMol(mol)  # MolFromSmiles does Sanitize by default
    mesomers = ResonanceEnumerator().enumerate(mol)
    return {Chem.MolToSmiles(m, isomericSmiles=True) for m in mesomers}


# -*- coding: utf-8 -*-
"""
molvs.normalize
~~~~~~~~~~~~~~~

This module contains tools for normalizing molecules using reaction SMARTS patterns.

"""

from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division
import logging

from rdkit import Chem
from rdkit.Chem import AllChem
import six

from .utils import memoized_property

log = logging.getLogger(__name__)

class Normalization(object):
    """A normalization transform defined by reaction SMARTS."""

    def __init__(self, name, transform):
        """
        :param string name: A name for this Normalization
        :param string transform: Reaction SMARTS to define the transformation.
        """
        log.debug('Initializing Normalization: %s', name)
        self.name = name
        self.transform_str = transform

    @memoized_property
    def transform(self):
        log.debug('Loading Normalization transform: %s', self.name)
        return AllChem.ReactionFromSmarts(str(self.transform_str))

    def __repr__(self):
        return 'Normalization({!r}, {!r})'.format(self.name, self.transform_str)

    def __str__(self):
        return self.name

#: The default list of Normalization transforms.
NORMALIZATIONS = (
    # Opposite of #2.1 in InChI technical manual? Covered by RDKit Sanitization.
    Normalization('Nitro to N+(O-)=O', '[N,P,As,Sb;X3:1](=[O,S,Se,Te:2])=[O,S,Se,Te:3]>>[*+1:1]([*-1:2])=[*:3]'),
    Normalization('Sulfone to S(=O)(=O)', '[S+2:1]([O-:2])([O-:3])>>[S+0:1](=[O-0:2])(=[O-0:3])'),
    Normalization('Pyridine oxide to n+O-', '[n:1]=[O:2]>>[n+:1][O-:2]'),
    Normalization('Azide to N=N+=N-', '[*,H:1][N:2]=[N:3]#[N:4]>>[*,H:1][N:2]=[N+:3]=[N-:4]'),
    Normalization('Diazo/azo to =N+=N-', '[*:1]=[N:2]#[N:3]>>[*:1]=[N+:2]=[N-:3]'),
    Normalization('Sulfoxide to -S+(O-)-', '[!O:1][S+0;X3:2](=[O:3])[!O:4]>>[*:1][S+1:2]([O-:3])[*:4]'),
    # Equivalent to #1.5 in InChI technical manual
    Normalization('Phosphate to P(O-)=O', '[O,S,Se,Te;-1:1][P+;D4:2][O,S,Se,Te;-1:3]>>[*+0:1]=[P+0;D5:2][*-1:3]'),
    # Equivalent to #1.8 in InChI technical manual
    Normalization('C/S+N to C/S=N+', '[C,S;X3+1:1]([NX3:2])[NX3!H0:3]>>[*+0:1]([N:2])=[N+:3]'),
    # Equivalent to #1.8 in InChI technical manual
    Normalization('P+N to P=N+', '[P;X4+1:1]([NX3:2])[NX3!H0:3]>>[*+0:1]([N:2])=[N+:3]'),
    Normalization('Normalize hydrazine-diazonium', '[CX4:1][NX3H:2]-[NX3H:3][CX4:4][NX2+:5]#[NX1:6]>>[CX4:1][NH0:2]=[NH+:3][C:4][N+0:5]=[NH:6]'),
    # Equivalent to #1.3 in InChI technical manual
    Normalization('Recombine 1,3-separated charges', '[N,P,As,Sb,O,S,Se,Te;-1:1]-[A+0:2]=[N,P,As,Sb,O,S,Se,Te;+1:3]>>[*-0:1]=[*:2]-[*+0:3]'),
    Normalization('Recombine 1,3-separated charges', '[n,o,p,s;-1:1]:[a:2]=[N,O,P,S;+1:3]>>[*-0:1]:[*:2]-[*+0:3]'),
    Normalization('Recombine 1,3-separated charges', '[N,O,P,S;-1:1]-[a:2]:[n,o,p,s;+1:3]>>[*-0:1]=[*:2]:[*+0:3]'),
    Normalization('Recombine 1,5-separated charges', '[N,P,As,Sb,O,S,Se,Te;-1:1]-[A+0:2]=[A:3]-[A:4]=[N,P,As,Sb,O,S,Se,Te;+1:5]>>[*-0:1]=[*:2]-[*:3]=[*:4]-[*+0:5]'),
    Normalization('Recombine 1,5-separated charges', '[n,o,p,s;-1:1]:[a:2]:[a:3]:[c:4]=[N,O,P,S;+1:5]>>[*-0:1]:[*:2]:[*:3]:[c:4]-[*+0:5]'),
    Normalization('Recombine 1,5-separated charges', '[N,O,P,S;-1:1]-[c:2]:[a:3]:[a:4]:[n,o,p,s;+1:5]>>[*-0:1]=[c:2]:[*:3]:[*:4]:[*+0:5]'),
    # Conjugated cation rules taken from Francis Atkinson's standardiser. Those that can reduce aromaticity aren't included
    Normalization('Normalize 1,3 conjugated cation', '[N,O;+0!H0:1]-[A:2]=[N!$(*[O-]),O;+1H0:3]>>[*+1:1]=[*:2]-[*+0:3]'),
    Normalization('Normalize 1,3 conjugated cation', '[n;+0!H0:1]:[c:2]=[N!$(*[O-]),O;+1H0:3]>>[*+1:1]:[*:2]-[*+0:3]'),
    #Normalization('Normalize 1,3 conjugated cation', '[N,O;+0!H0:1]-[c:2]:[n!$(*[O-]),o;+1H0:3]>>[*+1:1]=[*:2]:[*+0:3]'),
    Normalization('Normalize 1,5 conjugated cation', '[N,O;+0!H0:1]-[A:2]=[A:3]-[A:4]=[N!$(*[O-]),O;+1H0:5]>>[*+1:1]=[*:2]-[*:3]=[*:4]-[*+0:5]'),
    Normalization('Normalize 1,5 conjugated cation', '[n;+0!H0:1]:[a:2]:[a:3]:[c:4]=[N!$(*[O-]),O;+1H0:5]>>[n+1:1]:[*:2]:[*:3]:[*:4]-[*+0:5]'),
    # Normalization('Normalize 1,5 conjugated cation', '[N,O;+0!H0:1]-[c:2]:[a:3]:[a:4]:[n!$(*[O-]),o;+1H0:5]>>[*+1:1]=[c:2]:[*:3]:[*:4]:[*+0:5]'),
    # Normalization('Normalize 1,5 conjugated cation', '[n;+0!H0:1]1:[a:2]:[a:3]:[a:4]:[n!$(*[O-]);+1H0:5]1>>[n+1:1]1:[*:2]:[*:3]:[*:4]:[n+0:5]1'),
    # Normalization('Normalize 1,5 conjugated cation', '[n;+0!H0:1]:[a:2]:[a:3]:[a:4]:[n!$(*[O-]);+1H0:5]>>[n+1:1]:[*:2]:[*:3]:[*:4]:[n+0:5]'),
    # Equivalent to #1.6 in InChI technical manual. RDKit Sanitization handles this for perchlorate.
    Normalization('Charge normalization', '[F,Cl,Br,I,At;-1:1]=[O:2]>>[*-0:1][O-:2]'),
    Normalization('Charge recombination', '[N,P,As,Sb;-1:1]=[C+;v3:2]>>[*+0:1]#[C+0:2]'),
)
# InChI technical manual has many additional rules that cover situations that are disallowed by RDKit

#: The default value for the maximum number of times to attempt to apply the series of normalizations.
MAX_RESTARTS = 200

class Normalizer(object):
    """A class for applying Normalization transforms.

    This class is typically used to apply a series of Normalization transforms to correct functional groups and
    recombine charges. Each transform is repeatedly applied until no further changes occur.
    """

    def __init__(self, normalizations=NORMALIZATIONS, max_restarts=MAX_RESTARTS):
        """Initialize a Normalizer with an optional custom list of :class:`~molvs.normalize.Normalization` transforms.

        :param normalizations: A list of  :class:`~molvs.normalize.Normalization` transforms to apply.
        :param int max_restarts: The maximum number of times to attempt to apply the series of normalizations (default
                                 200).
        """
        log.debug('Initializing Normalizer')
        self.normalizations = normalizations
        self.max_restarts = max_restarts

    def __call__(self, mol):
        """Calling a Normalizer instance like a function is the same as calling its normalize(mol) method."""
        return self.normalize(mol)

    def normalize(self, mol):
        """Apply a series of Normalization transforms to correct functional groups and recombine charges.

        A series of transforms are applied to the molecule. For each Normalization, the transform is applied repeatedly
        until no further changes occur. If any changes occurred, we go back and start from the first Normalization
        again, in case the changes mean an earlier transform is now applicable. The molecule is returned once the entire
        series of Normalizations cause no further changes or if max_restarts (default 200) is reached.

        :param mol: The molecule to normalize.
        :type mol: rdkit.Chem.rdchem.Mol
        :return: The normalized fragment.
        :rtype: rdkit.Chem.rdchem.Mol
        """
        log.debug('Running Normalizer')
        # Normalize each fragment separately to get around quirky RunReactants behaviour
        fragments = []
        for fragment in Chem.GetMolFrags(mol, asMols=True):
            fragments.append(self._normalize_fragment(fragment))
        # Join normalized fragments into a single molecule again
        outmol = fragments.pop()
        for fragment in fragments:
            outmol = Chem.CombineMols(outmol, fragment)
        Chem.SanitizeMol(outmol)
        return outmol

    def _normalize_fragment(self, mol):
        for n in six.moves.range(self.max_restarts):
            # Iterate through Normalization transforms and apply each in order
            for normalization in self.normalizations:
                product = self._apply_transform(mol, normalization.transform)
                if product:
                    # If transform changed mol, go back to first rule and apply each again
                    log.info('Rule applied: %s', normalization.name)
                    mol = product
                    break
            else:
                # For loop finishes normally, all applicable transforms have been applied
                return mol
        # If we're still going after max_restarts (default 200), stop and warn, but still return the mol
        log.warning('Gave up normalization after %s restarts', self.max_restarts)
        return mol

    def _apply_transform(self, mol, rule):
        """Repeatedly apply normalization transform to molecule until no changes occur.

        It is possible for multiple products to be produced when a rule is applied. The rule is applied repeatedly to
        each of the products, until no further changes occur or after 20 attempts. If there are multiple unique products
        after the final application, the first product (sorted alphabetically by SMILES) is chosen.
        """
        mols = [mol]
        for n in six.moves.range(20):
            products = {}
            for mol in mols:
                for product in [x[0] for x in rule.RunReactants((mol,))]:
                    if Chem.SanitizeMol(product, catchErrors=True) == 0:
                        products[Chem.MolToSmiles(product, isomericSmiles=True)] = product
            if products:
                mols = [products[s] for s in sorted(products)]
            else:
                # If n == 0, the rule was not applicable and we return None
                return mols[0] if n > 0 else None