#!/bin/env python
from matplotlib import pyplot

from fabtotum import gerber
from fabtotum.gerber.render import *
from fabtotum.toolpath import *
from fabtotum.gcode import *

from shapely.geometry import Point

BLUE = '#6699cc'
RED =  '#ff0000'
GREEN =  '#00cc00'
GRAY = '#999999'

colors = ['#ff0000','#00cc00', '#0000cc', '#cccc00', '#cc00cc', '#00cccc']

def plot_rect(ax, x,y,w,h, color=GRAY, width=0.5, mirror_x=0):
    if mirror_x:
        x = [x,x-w,x-w,x,x]
        y = [y,y,y+h,y+h,y]
    else:
        x = [x,x+w,x+w,x,x]
        y = [y,y,y+h,y+h,y]
    ax.plot(x, y, color=color, linewidth=width, solid_capstyle='round', zorder=1)

def plot_points(ax, pts, color=GRAY, style='o'):
    x = []
    y = []
    for p in pts:
        x.append( p[0] )
        y.append( p[1] )
    ax.plot(x, y, style, color=color, zorder=1)

def plot_line(ax, ob, color=GRAY, width=0.5):
    x, y = ob.xy
    ax.plot(x, y, color=color, linewidth=width, solid_capstyle='round', zorder=1)

def plot_line_xy(ax, x, y, color=GRAY, width=0.5):
    ax.plot(x, y, color=color, linewidth=width, solid_capstyle='round', zorder=1)

def plot_coords(ax, ob):
    x, y = ob.xy
    ax.plot(x, y, 'o', color='#999999', zorder=1)

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

config = {

}

#~ front = gerber.read('milling/router_board_v1-F.Cu.gtl')
#~ back  = gerber.read('milling/router_board_v1-Edge.Cuts.gm1')
#~ edge  = gerber.read('milling/router_board_v1-B.Cu.gbl')
#~ drill = gerber.read('milling/router_board_v1.drl')

# 2----3
# |    |
# |    |
# 1----4
# (x,y), (1,2,3,4)

markers = ( (70,23),    #1
            (70,177),   #2
            (164,177),  #3
            (164,23) )  #4

marker_mid = ( (markers[0][0] + markers[2][0])/2.0,
               (markers[0][1] + markers[1][1])/2.0)

fig = pyplot.figure(1, figsize=(8,8), dpi=90)
ax = fig.add_subplot(111)

bed_bounds = (214,234)

#~ xrange = [-5,bed_bounds[0]+5]
xrange = [-5,bed_bounds[0]+5]
yrange = [-5,bed_bounds[1]+5]

ax.set_xlim(*xrange)
ax.set_ylim(*yrange)
ax.set_aspect(1)

plot_rect(ax, 0, 0, bed_bounds[0], bed_bounds[1], color=GRAY)
plot_rect(ax, markers[0][0]-3, markers[0][1]-3, 100, 160, color=GRAY)
plot_line_xy(ax, (marker_mid[0],marker_mid[0]), (bed_bounds[1],0), color=RED )
plot_points(ax, markers, color=RED, style='+')

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

pcb = gerber.PCB.from_directory('../common/pcb_1', verbose=False)

origin = (70,23)
bounds = (pcb.board_bounds[0][1],pcb.board_bounds[1][1])
plot_rect(ax, origin[0], origin[1], bounds[0], bounds[1], color=GRAY)

# mirror_x
origin_mirror_x = (2* marker_mid[0] - origin[0], origin[1])
plot_rect(ax, origin_mirror_x[0], origin_mirror_x[1], bounds[0], bounds[1], color=GRAY, mirror_x=1)

# Drill holes
for d in pcb.drill_layers[0].primitives:
    p = d.position

    p = (p[0] + origin[0], p[1] + origin[1])

    pt = Point(p).buffer(d.radius, cap_style=1, join_style=1)
    line = pt.exterior
    plot_line(ax, line, color=BLUE)

shapes = {}

for layer in pcb.layers:
    shp = ShapelyContext()
    layer.cam_source.render( shp )
    shapes[layer.layer_class] = shp

    if layer.layer_class == 'bottom':
        shp.mirror_x()
    elif layer.layer_class == 'outline':
		shp.set_ignore_width(True)
        #~ shp.translete(origin_mirror_x[0], origin_mirror_x[1])
    #~ else:
        #~ shp.translete(origin[0], origin[1])
    #~ print "origin",layer.layer_class,shp.origin

    for f in shp.figs:
        line = f.exterior
        plot_line(ax, line)

#pyplot.show()

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

class Output(object):

    def __init__(self, filename):
        self.filename = filename
        self.fd = open(filename, 'w')

    def write(self, v):
        self.fd.write(v + '\r\n')

# Prepare copper layer milling
for layer in pcb.copper_layers:
    out = Output('output/'+layer.layer_class+'.gcode')

    toolpath = IsolationToolpath()
    toolpath.add_tool(0.4)
    paths = toolpath.generate(shapes[layer.layer_class].figs)

    cnc = MillingPCB(out)

    # Start code
    cnc.setTravelSpeed(XY=5000, Z=1000)
    cnc.setMillingSpeed(150)
    cnc.setDrillSpeed(80)
    cnc.setTravelDistance(2)
    cnc.setPlungeDepth(0.35)
    cnc.setSpindleSpeed(15000)

    cnc.zeroZtoTool()

    cnc.setRelative()
    cnc.spindleON()

    # Milling code
    if paths:
        i = 1
        for tlp in paths:
            cnc.addComment('Path #' + str(i) )
            i += 1
            cnc.millPath(tlp)

    # End code
    cnc.stopMilling()
    cnc.spindleOFF()

# Prepare drilling
for layer in pcb.drill_layers:
    for drill in layer.drills:
        out = Output('output/'+layer.layer_class+'_'+str(drill)+'.gcode')

        cnc = MillingPCB(out)

        # Start code
        cnc.setTravelSpeed(XY=5000, Z=1000)
        cnc.setMillingSpeed(150)
        cnc.setDrillSpeed(50)
        cnc.setTravelDistance(2)
        cnc.setPlungeDepth(2)
        cnc.setSpindleSpeed(15000)

        cnc.zeroZtoTool()

        cnc.setRelative()
        cnc.spindleON()

        # Drilling
        cnc.addComment('Drill ' + str(drill) + 'mm' )
        for hole in layer.primitives:
            if hole.diameter == drill:
                p = hole.position
                cnc.drillAt(X=p[0], Y=p[1])

        # End code
        cnc.stopMilling()
        cnc.spindleOFF()

# Prepare cutting
for layer in pcb.outline_layers:
	out = Output('output/'+layer.layer_class+'.gcode')


# util/deprecations.py
# Copyright (C) 2005-2019 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php

"""Helpers related to deprecation of functions, methods, classes, other
functionality."""

import re
import warnings

from . import compat
from .langhelpers import decorator
from .langhelpers import inject_docstring_text
from .langhelpers import inject_param_text
from .. import exc

def warn_deprecated(msg, stacklevel=3):
    warnings.warn(msg, exc.SADeprecationWarning, stacklevel=stacklevel)

def warn_pending_deprecation(msg, stacklevel=3):
    warnings.warn(msg, exc.SAPendingDeprecationWarning, stacklevel=stacklevel)

def deprecated_cls(version, message, constructor="__init__"):
    header = ".. deprecated:: %s %s" % (version, (message or ""))

    def decorate(cls):
        return _decorate_cls_with_warning(
            cls,
            constructor,
            exc.SADeprecationWarning,
            message % dict(func=constructor),
            header,
        )

    return decorate

def deprecated(version, message=None, add_deprecation_to_docstring=True):
    """Decorates a function and issues a deprecation warning on use.

    :param version:
      Issue version in the warning.

    :param message:
      If provided, issue message in the warning.  A sensible default
      is used if not provided.

    :param add_deprecation_to_docstring:
      Default True.  If False, the wrapped function's __doc__ is left
      as-is.  If True, the 'message' is prepended to the docs if
      provided, or sensible default if message is omitted.

    """

    if add_deprecation_to_docstring:
        header = ".. deprecated:: %s %s" % (version, (message or ""))
    else:
        header = None

    if message is None:
        message = "Call to deprecated function %(func)s"

    def decorate(fn):
        return _decorate_with_warning(
            fn,
            exc.SADeprecationWarning,
            message % dict(func=fn.__name__),
            header,
        )

    return decorate

def deprecated_params(**specs):
    """Decorates a function to warn on use of certain parameters.

    e.g. ::

        @deprecated_params(
            weak_identity_map=(
                "0.7",
                "the :paramref:`.Session.weak_identity_map parameter "
                "is deprecated."
            )

        )

    """

    messages = {}
    for param, (version, message) in specs.items():
        messages[param] = _sanitize_restructured_text(message)

    def decorate(fn):
        spec = compat.inspect_getfullargspec(fn)
        if spec.defaults is not None:
            defaults = dict(
                zip(
                    spec.args[(len(spec.args) - len(spec.defaults)) :],
                    spec.defaults,
                )
            )
            check_defaults = set(defaults).intersection(messages)
            check_kw = set(messages).difference(defaults)
        else:
            check_defaults = ()
            check_kw = set(messages)

        has_kw = spec.varkw is not None

        @decorator
        def warned(fn, *args, **kwargs):
            for m in check_defaults:
                if kwargs[m] != defaults[m]:
                    warnings.warn(
                        messages[m], exc.SADeprecationWarning, stacklevel=3
                    )
            for m in check_kw:
                if m in kwargs:
                    warnings.warn(
                        messages[m], exc.SADeprecationWarning, stacklevel=3
                    )

            return fn(*args, **kwargs)

        doc = fn.__doc__ is not None and fn.__doc__ or ""
        if doc:
            doc = inject_param_text(
                doc,
                {
                    param: ".. deprecated:: %s %s" % (version, (message or ""))
                    for param, (version, message) in specs.items()
                },
            )
        decorated = warned(fn)
        decorated.__doc__ = doc
        return decorated

    return decorate

def pending_deprecation(
    version, message=None, add_deprecation_to_docstring=True
):
    """Decorates a function and issues a pending deprecation warning on use.

    :param version:
      An approximate future version at which point the pending deprecation
      will become deprecated.  Not used in messaging.

    :param message:
      If provided, issue message in the warning.  A sensible default
      is used if not provided.

    :param add_deprecation_to_docstring:
      Default True.  If False, the wrapped function's __doc__ is left
      as-is.  If True, the 'message' is prepended to the docs if
      provided, or sensible default if message is omitted.
    """

    if add_deprecation_to_docstring:
        header = ".. deprecated:: %s (pending) %s" % (version, (message or ""))
    else:
        header = None

    if message is None:
        message = "Call to deprecated function %(func)s"

    def decorate(fn):
        return _decorate_with_warning(
            fn,
            exc.SAPendingDeprecationWarning,
            message % dict(func=fn.__name__),
            header,
        )

    return decorate

def deprecated_option_value(parameter_value, default_value, warning_text):
    if parameter_value is None:
        return default_value
    else:
        warn_deprecated(warning_text)
        return parameter_value

def _sanitize_restructured_text(text):
    def repl(m):
        type_, name = m.group(1, 2)
        if type_ in ("func", "meth"):
            name += "()"
        return name

    return re.sub(r"\:(\w+)\:`~?\.?(.+?)`", repl, text)

def _decorate_cls_with_warning(
    cls, constructor, wtype, message, docstring_header=None
):
    doc = cls.__doc__ is not None and cls.__doc__ or ""
    if docstring_header is not None:
        docstring_header %= dict(func=constructor)

        doc = inject_docstring_text(doc, docstring_header, 1)

        if type(cls) is type:


#!/usr/bin/env python

# imports------------------------------------------------------
import sys, pickle

try:
    import UtilityFunction as UF
except ImportError as IR:
    sys.exit("Import Error: %s"%(str(IR)))

from time import time
from copy import deepcopy

# --------------------------------------------------------------

class Blowfish(object):
    N       = 16
    counter = ''
    data    = ''

    P_Box =  [0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344,
              0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89,
              0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
              0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917,
              0x9216d5d9, 0x8979fb1b]

    S_Box = ([0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, # sbox1
              0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99,
              0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16,
              0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e,
              0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee,
              0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
              0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef,
              0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e,
              0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60,
              0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440,
              0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce,
              0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
              0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e,
              0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677,
              0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193,
              0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032,
              0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88,
              0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
              0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e,
              0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0,
              0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3,
              0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,
              0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88,
              0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
              0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6,
              0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d,
              0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b,
              0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7,
              0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba,
              0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
              0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f,
              0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09,
              0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3,
              0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb,
              0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279,
              0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
              0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab,
              0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82,
              0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db,
              0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573,
              0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0,
              0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
              0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790,
              0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8,
              0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4,
              0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0,
              0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7,
              0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
              0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad,
              0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1,
              0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299,
              0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,
              0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477,
              0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
              0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49,
              0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af,
              0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa,
              0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5,
              0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41,
              0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
              0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400,
              0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915,
              0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664,
              0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a,
             ],
             [0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, # sbox2
              0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266,
              0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,
              0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e,
              0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6,
              0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
              0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e,
              0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1,
              0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737,
              0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8,
              0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff,
              0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
              0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701,
              0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7,
              0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41,
              0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331,
              0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf,
              0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
              0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e,
              0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87,
              0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c,
              0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2,
              0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16,
              0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
              0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b,
              0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509,
              0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e,
              0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3,
              0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f,
              0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
              0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4,
              0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960,
              0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,
              0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28,
              0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802,
              0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
              0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510,
              0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf,
              0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14,
              0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e,
              0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50,
              0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
              0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8,
              0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281,
              0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99,
              0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696,
              0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128,
              0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
              0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0,
              0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0,
              0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105,
              0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250,
              0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3,
              0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
              0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00,
              0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061,
              0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb,
              0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e,
              0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735,
              0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
              0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9,
              0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340,
              0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,
              0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7,
             ],
             [0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, # sbox3
              0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068,
              0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af,
              0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840,
              0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45,
              0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
              0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a,
              0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb,
              0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee,
              0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6,
              0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42,
              0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
              0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2,
              0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,
              0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527,
              0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b,
              0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33,
              0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
              0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3,
              0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc,
              0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17,
              0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564,
              0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b,
              0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
              0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922,
              0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728,
              0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0,
              0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e,
              0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37,
              0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
              0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804,
              0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b,
              0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3,
              0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb,
              0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d,
              0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
              0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350,
              0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9,
              0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a,
              0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe,
              0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d,
              0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
              0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f,
              0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,
              0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2,
              0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9,
              0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2,
              0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
              0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e,
              0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633,
              0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10,
              0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169,
              0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52,
              0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
              0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5,
              0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62,
              0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634,
              0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76,
              0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24,
              0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
              0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4,
              0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c,
              0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837,
              0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0,
             ],
             [0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, # sbox4
              0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe,
              0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b,
              0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4,
              0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8,
              0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
              0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304,
              0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22,
              0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4,
              0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,
              0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9,
              0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
              0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593,
              0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51,
              0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28,
              0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c,
              0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b,
              0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
              0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c,
              0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd,
              0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a,
              0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319,
              0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb,
              0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
              0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991,
              0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32,
              0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680,
              0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166,
              0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae,
              0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
              0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5,
              0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47,
              0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370,
              0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d,
              0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84,
              0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
              0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8,
              0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd,
              0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9,
              0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,
              0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38,
              0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
              0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c,
              0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525,
              0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1,
              0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442,
              0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964,
              0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
              0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8,
              0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d,
              0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f,
              0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299,
              0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02,
              0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
              0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614,
              0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a,
              0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6,
              0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b,
              0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0,
              0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
              0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e,
              0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9,
              0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f,
              0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6,
             ],
            )

    def __init__(self, key, nonce = ''):
        """ creates an instance of Blowfish and sets up the
            key by dispersing its value through P and S.  A
            nonce (or initial counter value) sets up the
            8-byte counter.  If no nonce (or counter) is
            provided, the system date-time will be used as
            the default.

            key is a bytestring of between 8 and 56 bytes
            nonce is an optional bytestring of up to 8 bytes
        """
        if 8 < len(key) > 56:
            raise Exception('Key length not 8..56 bytes')

        # deep copy lest we modify class variables
        self.P = deepcopy(self.P_Box)
        self.S = deepcopy(self.S_Box)

        # expand the key to fully cover 18 4-byte words.  It
        # is okay if the exceed that by a small number as we
        # will only use what we need.
        key = (key * (18 * 8 // len(key) + 1))
        # apply the key to P
        for i in xrange(18):
            self.P[i] ^= UF.fourByte2int(key[i*4:i*4+4])

        # now diffuse the key throughout S
        word = chr(0)*8
        for i in xrange(0, 18, 2):
            word = self.encryptBlock(word)
            intword = UF.eightByte2int(word)
            self.P[i]   = intword >> 32
            self.P[i+1] = intword & 0xFFFFFFFF
        for i in xrange(4):
            for j in xrange(0, 256, 2):
                word = self.encryptBlock(word)
                intword = UF.eightByte2int(word)
                self.S[i][j]   = intword >> 32
                self.S[i][j+1] = intword & 0xFFFFFFFF

        self.set_counter(nonce)     # only necessary if CTR mode

    def encryptBlock(self, block):
        """ encrypt a single block of data

            data is to be a bytestring of exactly 8 bytes

            encipher_block() used to encrypt and decrypt when
            in CTR mode
        """
        xl = UF.fourByte2int(block[:4])
        xr = UF.fourByte2int(block[4:])
        for i in xrange(self.N):
            xl = xl ^ self.P[i]
            xr = self._F(xl) ^ xr
            xl, xr = xr, xl
        xl, xr = xr, xl
        xr = xr ^ self.P[self.N]
        xl = xl ^ self.P[self.N+1]
        return UF.int2fourByte(xl) + UF.int2fourByte(xr)

    def decryptBlock(self, block):
        """ decrypt a single block of data

            data is to be a bytestring of exactly 8 bytes

            decipher_block() not needed when in CTR mode
        """
        xl = UF.fourByte2int(block[:4])
        xr = UF.fourByte2int(block[4:])
        for i in xrange(17, 1, -1):
            xl = xl ^ self.P[i]
            xr = self._F(xl) ^ xr
            xl, xr = xr, xl
        xl, xr = xr, xl
        xr = xr ^ self.P[1]
        xl = xl ^ self.P[0]
        return UF.int2fourByte(xl) + UF.int2fourByte(xr)

    def _F(self, x):
        "F Function for scrambling the data"
        a = (x & 0xFF000000) >> 24
        b = (x & 0x00FF0000) >> 16
        c = (x & 0x0000FF00) >>  8
        d = (x & 0x000000FF)
        # y = ((S[0][a] + S[1][b]) ^ S[2][c]) + S[3][d]
        y = (self.S[0][a] + self.S[1][b]) & 0xFFFFFFFF
        y =  y ^ self.S[2][c]
        y = (y + self.S[3][d]) & 0xFFFFFFFF
        return y

    def set_counter(self, counter=''):
        "sets the initial counter value"
        if not counter:
            # use date-time if no counter provided
            counter = UF.int2fourByte(int(time()) & 0xFFFFFFFF)
        # pad with nulls as needed
        self.counter = (counter+chr(0)*8)[:8]
        self.counter0 = self.counter

    def get_counter0(self):
        "returns the initial counter value"
        return self.counter0

    def _incr_counter(self):
        # this is faster than calculating indexes for a loop
        counter = [ch for ch in self.counter]
        counter[7] = chr((ord(counter[7])+1)&0xFF)
        if counter[7] == chr(0):
            counter[6] = chr((ord(counter[6])+1)&0xFF)
            if counter[6] == chr(0):
                counter[5] = chr((ord(counter[5])+1)&0xFF)
                if counter[5] == chr(0):
                    counter[4] = chr((ord(counter[4])+1)&0xFF)
        self.counter = ''.join(counter)

    def CTR_update(self, data):
        """ CTR_update() may be called multiple times if need
            to chunk large datasets

            data is a bytestring of any reasonable length
        """
        self.data = self.data + data
        ciphertext = []
        while len(self.data) > 8:
            cipherstream = self.encryptBlock(self.counter)
            ciphertext.append(UF.xor(cipherstream, self.data[:8]))


import numpy as np
import copy
import pele.utils.rotations as rot
import itertools

__all__ = ["alignCoM", "CoMToOrigin", "getAlignRotation", "alignRotation",
           "findBestPermutationRBMol", "aa2xyz",
           "getDistxyz", "getDistaa"]

def alignCoM( X1, X2):
    """
    align the center of mass of X2 with that of X1
    """
    natoms = len(X1) / 3
    for i in xrange(3):
        com = np.sum( X1[i::3] - X2[i::3] )
        com /= natoms
        X2[i::3] += com

def CoMToOrigin( X1):
    """
    move the center of mass to the origin
    """
    X1 = np.reshape(X1, [-1,3])
    natoms = len(X1[:,0])
    com = X1.sum(0) / natoms
    X1 -= com
    return X1.reshape(-1)

def alignRotation(XA, XB):
    """
    Align structure XB with XA

    Align structure XB to be as similar as possible to structure XA.
    To be precise, rotate XB, so as to minimize the distance |XA - XB|.

    Rotations will be done around the origin, not the center of mass
    """
    nsites = len(XA)/3

    dist, Q2 = getAlignRotation(XA, XB)
    ###################################################################
    # Q2 contains the quaternion which rotates XB to best align with X1.
    # rotate XB according to Q2
    ###################################################################

    rot_mx = rot.q2mx( Q2 )
    #print rot_mx
    for j in range(nsites):
        i = 3*j
        XB[i:i+3] = np.dot( rot_mx, XB[i:i+3] )

    dist = np.linalg.norm(XA - XB) #more precise than what it returned

    return dist, XB

#def findBestPermutation2( X1, X2, permlist = [] ):
#    """
#    find the permutation of the atoms which minimizes the distance |X1-X2|
#    """
#    if len(permlist) == 0:
#        permlist = [range(len(X1)/3)]
#    for atomlist in permlist:
#        dist, X1, X2 = findBestPermutationList2( X1, X2, atomlist )
#    dist = np.linalg.norm(X1-X2)
#    return dist, X1, X2

def molmolMinSymDist(com1, aa1, com2, aa2, mol):
    """
    return the minimal distance between two molecules
    defined by mol and com1, aa1 and mol, com2, aa2

    Don't rotate or translate, just apply inner-molecular symmetry operations
    """
    xyz1 = mol.getxyz(com1, aa1)
    mindist = 10000.
    for xyz2, newaa in mol.getSymmetries(com2, aa2):
        dist = np.linalg.norm(xyz1 - xyz2)
        if dist  < mindist:
            mindist = dist
            aamin = newaa.copy()
    return mindist, aamin

def getDistxyz( xyz1, xyz2 ):
    return np.linalg.norm(xyz1 - xyz2)

def getDistaa(coords1, coords2, mysys):
    xyz1 = mysys.getxyz(coords1)
    xyz2 = mysys.getxyz(coords2)
    return getDistxyz(xyz1, xyz2)

def findBestPermutationRBMol_list(coords1, coords2, mol, mollist):
    """
    find the permutation of the molecules which minimizes the distance between the two coordinates
    """
    nmol = len(coords1) / 3 / 2
    nperm = len(mollist)
    coords2old = coords2.copy()
    #########################################
    # create the cost matrix
    #########################################
    cost = np.zeros( [nperm,nperm], np.float64)
    for i in range(nperm):
        imol = mollist[i]
        com1 = coords1[         imol*3 :          imol*3 + 3]
        aa1  = coords1[3*nmol + imol*3 : 3*nmol + imol*3 + 3]
        for j in range(nperm):
            jmol = mollist[j]
            com2 = coords2[         jmol*3 :          jmol*3 + 3]
            aa2  = coords2[3*nmol + jmol*3 : 3*nmol + jmol*3 + 3]
            cost[j,i], newaa = molmolMinSymDist(com1, aa1, com2, aa2, mol)

    #convert cost matrix to a form used by munkres
    matrix = cost.tolist()

    #########################################
    # run the hungarian algorithm
    #########################################
    try:
        #use the hungarian package which is compiled
        import hungarian
        newind1 = hungarian.lap(cost)
        newind = [(i, j) for i,j in enumerate(newind1[0])]
        #print "hungari newind", newind
    except ImportError:
        try:
            #use the munkres package
            #convert cost matrix to a form used by munkres
            from munkres import Munkres
            matrix = cost.tolist()
            m = Munkres()
            newind = m.compute(matrix)
            #print "munkres newind", newind
        except ImportError:
            print "ERROR: findBestPermutation> You must install either the hungarian or the munkres package to use the Hungarian algorithm"
            #raise Exception("ERROR: findBestPermutation> You must install either the hungarian or the munkres package to use the Hungarian algorithm")
            dist = np.linalg.norm( coords1 - coords2 )
            return dist, coords1, coords2

    #########################################
    # apply the permutation
    #########################################
    costnew = 0.;
    coords2 = coords2old.copy()
    for (iold, inew) in newind:
        costnew    += cost[iold,inew]
        if iold != inew:
            moliold = mollist[iold]
            molinew = mollist[inew]
            #print "%4d  ->  %4d" % (moliold, molinew)
            #change the com coords
            coords2[         molinew*3 : molinew*3+3] = coords2old[         moliold*3 : moliold*3+3]
            #change the aa coords
            coords2[3*nmol + molinew*3 : 3*nmol + molinew*3+3] = coords2old[3*nmol + moliold*3 : 3*nmol + moliold*3+3]

    dist = np.sqrt(costnew)
    return dist, coords1, coords2

def findBestPermutationRBMol(coords1, coords2, mysys, permlist):
    """
    find the best permutation of the molecules.

    then, for each molecule, apply the symmetry operation which minimized the distance
    """
    nmol = len(coords1) / 3 / 2
    if permlist is None:
        permlist = [range(nmol)]
    for mollist in permlist:
        mol = mysys.molecule_list[ mollist[0] ] #a molecule object corresponding to this permuation
        dist, coords1, coords2 = findBestPermutationRBMol_list( coords1, coords2, mol, mollist )
    dist = getDistaa(coords1, coords2, mysys)
    #print "in findBest after perm", dist, mysys.getEnergy(coords2)

    #the molecules now have their correct permutation.
    #For each molecule, apply the symmetry operation which minimized the distance
    for i, mol in enumerate(mysys.molecule_list):
        kcom = 3*i
        kaa  = 3*nmol + 3*i
        com1 = coords1[kcom : kcom + 3]
        aa1  = coords1[kaa  : kaa  + 3]
        com2 = coords2[kcom : kcom + 3]
        aa2  = coords2[kaa  : kaa  + 3]
        d, aanew = molmolMinSymDist(com1, aa1, com2, aa2, mol)
        coords2[kaa : kaa + 3] = aanew
    dist = getDistaa(coords1, coords2, mysys)
    #print "after inner-mol sym", dist
    return dist, coords1, coords2

def aa2xyz(XB, AA):
    """
    Rotate XB according to angle axis AA
    """
    nsites = len(XB)/3


#!/usr/bin/python
# -*- coding: utf-8 -*-

# Clean fasta DNA sequence alignment for sequencing errors

__authors__ = "Eric Normandeau"
__program_name__ = "alignment_clean"
__version_info__ = ('0', '0', '1')
__version__ = '.'.join(__version_info__)
__copyright__ = "Copyright (c) 2011 Eric Normandeau"
__license__ = "GPLv3"
__revision_date__ = "2011-06-10"

# Importing modules

import os
import sys
import re
import getopt
import platform

try:
    from Bio import SeqIO
except:
    print "This program requires the Biopython library"
    sys.exit(0)

# Function definitions

def help():
    """Help attained by typing './program_name.py -h'
    """
    _plateform = platform.system()
    name = __program_name__
    text =  """
%s(1)            User Commands         %s(1)

\033[1mNAME\033[0m
\t%s - Clean fasta DNA sequence alignment for sequencing errors

\033[1mSYNOPSIS\033[0m
\t\033[1mpython %s.py \033[0m[\033[4mOPTIONS\033[0m]

\033[1mDESCRIPTION\033[0m
\tRemove bad regions/nucleotides from a fasta sequence alignment

\t%s takes a fasta alignment file for input and cleans it.

\033[1mOPTIONS\033[0m
\t\033[1m-h, --help\033[0m
\t\tDisplay the help of this program

\t\033[1m-i, --input\033[0m
\t\tInput file

\t\033[1m-o, --output\033[0m
\t\tOutput file

\t\033[1m-l, --lefttrim\033[0m
\t\tNumber of nucleotides to trim on the left

\t\033[1m-r, --righttrim\033[0m
\t\tNumber of nucleotides to trim on the right

\t\033[1m-I, --inserts\033[0m
\t\tRemove inserts found at less than a minimum frequency
\t\tNumber between 0 and 1 (eg: 0.01)

\t\033[1m-s, --snps\033[0m
\t\tRemove snp variants found at less than a minimum frequency
\t\tNumber between 0 and 1 (eg: 0.01)

\t\033[1m-p, --partial\033[0m
\t\tRemove partial duplicates
\t\tPatial duplicates are sequences that begin or end with '-'
\t\tcharacters but are otherwise identical to a sequence that
\t\tdoes not start or end with '-' characters

\t\033[1m-d, --duplicates\033[0m
\t\tRemove exact duplicates

\033[1mAUTHORS\033[0m
\t%s

%s %s           %s          %s(1)
"""%(name, name, name, name, name, __authors__, name, __version__, \
    __revision_date__, name)

    if _plateform != 'Windows' and "this is great news":
        print text
    else:
        __Windows__ = "This is an abomination"
        remove = ["\033[1m","\033[0m","\033[4m"]
        for i in remove:
            text = text.replace(i, "")
        print text
        del(__Windows__) # If only we could...

def short_help(msg):
    """Print short help in case of bad user input
    """
    print msg
    print "Use -h for help"
    sys.exit(0)

def trim_left(names, sequences, lefttrim):
#    print "Trimming left"
    assert lefttrim < len(sequences[0]), "Left trimming by longer than the sequences"
    return names, [s[lefttrim:] for s in sequences]

def trim_right(names, sequences, righttrim):
#    print "Trimming right"
    assert righttrim < len(sequences[0]), "Left trimming by longer than the sequences"
    return names, [s[:-righttrim] for s in sequences]

def remove_inserts(names, sequences, inserts):
#    print "Removing low frequency inserts"
    for i in sorted(range(len(sequences[0])), reverse=True):
        nuc = dict()
        nuc["A"] = nuc["C"] = nuc["G"] = nuc["T"] = nuc["-"] = 0
        for j in range(len(sequences)):
            nuc[sequences[j][i]] += 1
        nonuc = nuc["-"]
        nucs = [nuc["A"], nuc["C"], nuc["G"], nuc["T"]]
        if nonuc > max(nucs) and float(sum(nucs)) / (nonuc + sum(nucs)) < inserts:
            sequences = [s[:i] + s[i+1:] for s in sequences]
    return names, sequences

def remove_snps(names, sequences, snps):
#    print "Removing low frequency snps"
    for i in sorted(range(len(sequences[0])), reverse=True):
        nuc = dict()
        nuc["A"] = nuc["C"] = nuc["G"] = nuc["T"] = nuc["-"] = 0
        for j in range(len(sequences)):
            nuc[sequences[j][i]] += 1
        nuc.pop("-")
        most_frequent = "none"
        maximum = 0
        for n in nuc:
            if nuc[n] > maximum:
                maximum = nuc[n]
                most_frequent = n
        nucs = list(sorted([nuc["A"], nuc["C"], nuc["G"], nuc["T"]], reverse = True))
        if nucs[0] > nucs[1] and float((sum(nucs) - nucs[0])) / sum(nucs) < snps:
            sequences = [s[:i] + most_frequent + s[i+1:]
                         if s[i] != "-" else s for s in sequences]
    return names, sequences

def remove_partial(names, sequences, partial):
#    print "Removing redundant partial sequences"
    for i in sorted(range(len(sequences)), reverse=True):
        left_pad = len(re.findall("^-*", sequences[i])[0])
        right_pad = len(re.findall("-*$", sequences[i])[0])
        if left_pad > 0:
            temp_sequences = [s[left_pad:] for s in sequences]
            set_sequences = set(temp_sequences[:i] + temp_sequences[i+1:])
            if temp_sequences[i] in set_sequences:
                names = names[:i] + names[i+1:]
                sequences = sequences[:i] + sequences[i+1:]
        if right_pad > 0:
            temp_sequences = [s[:right_pad] for s in sequences]
            set_sequences = set(temp_sequences[:i] + temp_sequences[i+1:])
            if temp_sequences[i] in set_sequences:
                names = names[:i] + names[i+1:]
                sequences = sequences[:i] + sequences[i+1:]
    return names, sequences

def remove_duplicates(names, sequences):
#    print "Removing duplicate sequences"
    unique_sequences = set()
    for i in sorted(xrange(len(sequences)), reverse=True):
        seq = sequences[i]
        name = names[i]
        if seq not in unique_sequences:
            unique_sequences.add(seq)
        else:
            names = names[:i] + names[i+1:]
            sequences = sequences[:i] + sequences[i+1:]
    return names, sequences

def output_sequences(names, sequences, output_file):
    with open(output_file, "w") as f:
        for i, name in enumerate(names):
            f.write(">" + name + "\n" + sequences[i] + "\n")

# Does alignment_clean
# neatify your sequences
# up to your standards?

# The program itself

if __name__ == "__main__":
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hi:o:l:r:I:s:pd", ["help",
            "input=", "output=", "lefttrim=", "righttrim=", "inserts=",
            "snps=", "partial", "duplicate"])
    except getopt.GetoptError, e:
        short_help("Input error. Use -h for help")
    lefttrim = 0
    righttrim = 0
    inserts = 0
    snps = 0
    partial = False
    duplicate = False
    for option, value in opts:
        if option in ('-h', '--help'):
            help()
            sys.exit(0)
        elif option in ('-i', '--input'):
            input_file = value
            try:
                with open(input_file) as test:
                    pass
            except:
                short_help("Input Error: No input file specified or file not found.")
        elif option in ('-o', '--output'):
            output_file = value
            try:
                with open(output_file, "w") as test:
                    pass
            except:
                short_help("Output Error: No output file specified or incorect path.")
        elif option in ('-l', '--lefttrim'):
            try:
                lefttrim = int(value)
                assert lefttrim >= 0
            except:
                short_help("Positive integer needed for 'lefttrim' option")
        elif option in ('-r', '--righttrim'):
            try:
                righttrim = int(value)
                assert righttrim >= 0
            except:
                short_help("Positive integer needed for 'righttrim' option")
        elif option in ('-I', '--inserts'):
            try:
                inserts = float(value)
                assert 0 <= inserts <= 1
            except:
                short_help("Number between 0 and 1 needed for 'inserts' option")
        elif option in ('-s', '--snps'):
            try:
                snps = float(value)
                assert 0 <= snps <= 1
            except:
                short_help("Number between 0 and 1 needed for 'snps' option")
        elif option in ('-p', '--partial'):
            partial = True
        elif option in ('-d', '--duplicates'):
            duplicate = True

#    print __program_name__, __version__
#    print __copyright__
#    print

    fasta_sequences = SeqIO.parse(open(input_file),'fasta')
    names = []
    sequences = []

    for seq in fasta_sequences:
        names.append(seq.id)
        sequences.append(seq.seq.tostring())

#    print "  -", len(sequences[0]), len(sequences)
    if lefttrim > 0:
        names, sequences = trim_left(names, sequences, lefttrim)

#    print "  -", len(sequences[0]), len(sequences)
    if righttrim > 0:
        names, sequences = trim_right(names, sequences, righttrim)

#    print "  -", len(sequences[0]), len(sequences)
    names, sequences = remove_inserts(names, sequences, inserts)

#    print "  -", len(sequences[0]), len(sequences)
    names, sequences = remove_snps(names, sequences, snps)

#    print "  -", len(sequences[0]), len(sequences)
    if partial == True:


# -*- coding: utf-8 -*-
import json
from functools import wraps
from xmlrpc.client import Fault as RPCFault

from celery import chain, group
from celery.result import AsyncResult
from dateutil import parser
from django.contrib import messages
from django.contrib.auth import views
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db import transaction
from django.db.models import Q
from django.http import HttpResponse, Http404, HttpResponseForbidden
from django.shortcuts import render, redirect

from appliances.api import json_response
from appliances.models import (
    Provider, AppliancePool, Appliance, Group, Template, MismatchVersionMailer, User, BugQuery,
    GroupShepherd)
from appliances.tasks import (appliance_power_on, appliance_power_off, appliance_suspend,
    anyvm_power_on, anyvm_power_off, anyvm_suspend, anyvm_delete, delete_template_from_provider,
    appliance_rename, wait_appliance_ready, mark_appliance_ready, appliance_reboot,
    nuke_template_configuration)

from sprout.log import create_logger
from cfme.utils.bz import Bugzilla
from cfme.utils.providers import get_mgmt
from cfme.utils.version import Version

from wrapanapi import Openshift

def go_home(request):
    return redirect(index)

def go_back_or_home(request):
    ref = request.META.get('HTTP_REFERER')
    if ref:
        return redirect(ref)
    else:
        return go_home(request)

def only_authenticated(view):
    @wraps(view)
    def g(request, *args, **kwargs):
        if not request.user.is_authenticated():
            messages.error(
                request, 'You need to be authenticated to access "{}"'.format(request.path))
            return go_home(request)
        else:
            return view(request, *args, **kwargs)

    if not hasattr(g, '__wrapped__'):
        g.__wrapped__ = view
    return g

def logout(request):
    views.logout(request)
    messages.info(request, 'You have been logged out')
    return go_home(request)

def index(request):
    superusers = User.objects.filter(is_superuser=True).order_by("last_name", "first_name")
    return render(request, 'index.html', locals())

def providers(request, provider_id=None):
    if request.user.is_staff or request.user.is_superuser:
        user_filter = {}
    else:
        user_filter = {'user_groups__in': request.user.groups.all()}
    if provider_id is None:
        try:
            provider = Provider.objects.filter(hidden=False, **user_filter).order_by("id")[0]
            return redirect(
                "specific_provider",
                provider_id=provider.id)
        except IndexError:
            # No Provider
            messages.info(request, "No provider present, redirected to the homepage.")
            return go_home(request)
    else:
        try:
            provider = Provider.objects.filter(id=provider_id, **user_filter).distinct().first()
            if provider is None:
                messages.error(
                    request,
                    'Could not find a provider with name {} that you would have access to.'.format(
                        provider_id))
                return go_home(request)
            if provider.hidden:
                messages.warning(request, 'Provider {} is hidden.'.format(provider_id))
                return redirect('providers')
        except ObjectDoesNotExist:
            messages.warning(request, "Provider '{}' does not exist.".format(provider_id))
            return redirect("providers")
    providers = Provider.objects.filter(hidden=False, **user_filter).order_by("id").distinct()
    return render(request, 'appliances/providers.html', locals())

def provider_usage(request):
    complete_usage = Provider.complete_user_usage(request.user)
    total_usage = sum(count for pair in complete_usage for count in [int(pair[1])])
    return render(request, 'appliances/provider_usage.html', locals())

def templates(request, group_id=None, prov_id=None):
    if request.user.is_staff or request.user.is_superuser:
        user_filter = {}
        user_filter_2 = {}
    else:
        user_filter = {'user_groups__in': request.user.groups.all()}
        user_filter_2 = {'provider__user_groups__in': request.user.groups.all()}
    if group_id is None:
        try:
            return redirect("group_templates", group_id=Group.objects.order_by("id")[0].id)
        except IndexError:
            # No Group
            messages.info(request, "No group present, redirected to the homepage.")
            return go_home(request)
    else:
        try:
            group = Group.objects.get(id=group_id)
        except ObjectDoesNotExist:
            messages.warning(request, "Group '{}' does not exist.".format(group_id))
            return redirect("templates")
    if prov_id is not None:
        try:
            provider = Provider.objects.filter(id=prov_id, **user_filter).distinct().first()
        except ObjectDoesNotExist:
            messages.warning(request, "Provider '{}' does not exist.".format(prov_id))
            return redirect("templates")
    else:
        provider = None
    if provider is not None:
        user_filter_2 = {'provider': provider}
    groups = Group.objects.order_by("id")
    mismatched_versions = MismatchVersionMailer.objects.order_by("id")
    prepared_table = []
    zstream_rowspans = {}
    version_rowspans = {}
    date_version_rowspans = {}
    items = list(group.zstreams_versions.items())
    items.sort(key=lambda pair: Version(pair[0]), reverse=True)
    for zstream, versions in items:
        for version in versions:
            for template in Template.objects.filter(
                    template_group=group, version=version, exists=True,
                    ready=True, **user_filter_2).order_by('-date', 'provider').distinct():
                if zstream in zstream_rowspans:
                    zstream_rowspans[zstream] += 1
                    zstream_append = None
                else:
                    zstream_rowspans[zstream] = 1
                    zstream_append = zstream

                if version in version_rowspans:
                    version_rowspans[version] += 1
                    version_append = None
                else:
                    version_rowspans[version] = 1
                    version_append = version

                datetuple = (template.date, version)
                if datetuple in date_version_rowspans:
                    date_version_rowspans[datetuple] += 1
                    date_append = None
                else:
                    date_version_rowspans[datetuple] = 1
                    date_append = template.date
                prepared_table.append((
                    zstream_append, version_append, date_append, datetuple, template.provider,
                    template))
    return render(request, 'appliances/templates.html', locals())

@only_authenticated
def shepherd(request):
    groups = Group.objects.all()
    shepherds = GroupShepherd.objects.filter(
        template_group__in=groups, user_group__in=request.user.groups.all()).distinct().order_by(
        'template_group__id')
    return render(request, 'appliances/shepherd.html', locals())

@only_authenticated
def versions_for_group(request):
    group_id = request.POST.get("stream")
    template_type = request.POST.get("template_type")
    latest_version = None
    preconfigured = request.POST.get("preconfigured", "false").lower() == "true"
    if group_id == "<None>":
        versions = []
        group = None
    else:
        try:
            group = Group.objects.get(id=group_id)
        except ObjectDoesNotExist:
            versions = []
        else:
            filters = {
                'template_group': group,
                'ready': True,
                'usable': True,
                'exists': True,
                'provider__working': True,
                'provider__disabled': False,
                'provider__user_groups__in': request.user.groups.all(),
                'preconfigured': preconfigured,
                'template_type': template_type
            }

            versions = [
                (version, Template.ga_version(version))
                for version in Template.get_versions(**filters)]
            if versions:
                if versions[0][1]:
                    latest_version = '{} (GA)'.format(versions[0][0])
                else:
                    latest_version = versions[0][0]

    return render(request, 'appliances/_versions.html', locals())

@only_authenticated
def date_for_group_and_version(request):
    group_id = request.POST.get("stream")
    template_type = request.POST.get("template_type")
    latest_date = None
    preconfigured = request.POST.get("preconfigured", "false").lower() == "true"
    if group_id == "<None>":
        dates = []
    else:
        try:
            group = Group.objects.get(id=group_id)
        except ObjectDoesNotExist:
            dates = []
        else:
            version = request.POST.get("version")
            filters = {
                "template_group": group,
                "ready": True,
                "exists": True,
                "usable": True,
                "preconfigured": preconfigured,
                "provider__working": True,
                'provider__disabled': False,
                "provider__user_groups__in": request.user.groups.all(),
                'template_type': template_type
            }

            if version == "latest":
                try:
                    versions = Template.get_versions(**filters)
                    filters["version"] = versions[0]
                except IndexError:
                    pass  # No such thing as version for this template group
            else:
                filters["version"] = version
            dates = Template.get_dates(**filters)
            if dates:
                latest_date = dates[0]
    return render(request, 'appliances/_dates.html', locals())

@only_authenticated
def providers_for_date_group_and_version(request):
    total_provisioning_slots = 0
    total_appliance_slots = 0
    total_shepherd_slots = 0
    shepherd_appliances = {}
    group_id = request.POST.get("stream")
    provider_type = request.POST.get("provider_type")
    template_type = request.POST.get("template_type")
    if provider_type == 'any' or not provider_type:
        provider_type = None
    preconfigured = request.POST.get("preconfigured", "false").lower() == "true"

    if group_id == "<None>":
        providers = []
    else:
        try:
            group = Group.objects.get(id=group_id)
        except ObjectDoesNotExist:
            providers = []
        else:
            version = request.POST.get("version")
            filters = {
                "template_group": group,
                "ready": True,
                "exists": True,
                "usable": True,
                "preconfigured": preconfigured,
                "provider__working": True,
                "provider__disabled": False,
                "provider__user_groups__in": request.user.groups.all(),
                "template_type": template_type,
            }

            if version == "latest":
                try:
                    versions = Template.get_versions(**filters)
                    filters["version"] = versions[0]
                except IndexError:
                    pass  # No such thing as version for this template group
            else:
                filters["version"] = version
            date = request.POST.get("date")
            if date == "latest":
                try:
                    dates = Template.get_dates(**filters)
                    filters["date"] = dates[0]
                except IndexError:
                    pass  # No such thing as date for this template group
            else:
                filters["date"] = parser.parse(date)
            providers = Template.objects.filter(**filters).values("provider").distinct()
            providers = sorted([list(p.values())[0] for p in providers])
            providers = list(Provider.objects.filter(id__in=providers))
            if provider_type is None:
                providers = list(providers)
            else:
                providers = [
                    provider
                    for provider
                    in providers
                    if provider.provider_type == provider_type]
            for provider in providers:
                appl_filter = dict(
                    appliance_pool=None, ready=True, template__provider=provider,
                    template__preconfigured=filters["preconfigured"],
                    template__template_group=filters["template_group"],
                    template__template_type=filters["template_type"])
                if "date" in filters:
                    appl_filter["template__date"] = filters["date"]

                if "version" in filters:
                    appl_filter["template__version"] = filters["version"]

                shepherd_appliances[provider.id] = len(
                    Appliance.objects.filter(**appl_filter))
                total_shepherd_slots += shepherd_appliances[provider.id]
                total_appliance_slots += provider.remaining_appliance_slots
                total_provisioning_slots += provider.remaining_provisioning_slots

            render_providers = {}
            for provider in providers:
                render_providers[provider.id] = {
                    "shepherd_count": shepherd_appliances[provider.id], "object": provider}
    return render(request, 'appliances/_providers.html', locals())

@only_authenticated
def my_appliances(request, show_user="my"):
    if not request.user.is_superuser:
        if not (show_user == "my" or show_user == request.user.username):
            messages.info(request, "You can't view others' appliances!")
            return redirect("my_appliances")
        if show_user == request.user.username:
            return redirect("my_appliances")
    else:
        other_users = User.objects.exclude(pk=request.user.pk).order_by("last_name", "first_name")
    if show_user == "my":
        pools = AppliancePool.objects.filter(owner=request.user).order_by("id")
    elif show_user == "all":
        pools = AppliancePool.objects.order_by("id")
    else:
        pools = AppliancePool.objects.filter(owner__username=show_user).order_by("id")
    pools = pools.select_related('group', 'provider', 'owner')
    page = request.GET.get("page")
    try:
        per_page = int(request.GET.get("per_page", 25))
    except (ValueError, TypeError):
        per_page = 5

    pools_paginator = Paginator(pools, per_page)
    try:
        pools_paged = pools_paginator.page(page)
        page = int(page)
    except PageNotAnInteger:
        # If page is not an integer, deliver first page.
        pools_paged = pools_paginator.page(1)
        page = 1
    except EmptyPage:
        # If page is out of range (e.g. 9999), deliver last page of results.
        pools_paged = pools_paginator.page(pools_paginator.num_pages)
        page = pools_paginator.num_pages

    pages = list(pools_paginator.page_range)
    if pools_paginator.num_pages <= 5:
        start_index = 0
        end_index = 5
    else:
        if page - 2 < 0:
            start_index = 0
            end_index = 5
        elif page + 2 > pools_paginator.num_pages:
            end_index = pools_paginator.num_pages
            start_index = end_index - 5
        else:
            start_index = page - 3
            end_index = page + 2
    if start_index < 0:
        end_index -= start_index
        start_index = 0
    pages = pages[start_index:end_index]
    available_groups = Group.objects.filter(
        id__in=Template.objects.values_list('template_group', flat=True).distinct())
    group_tuples = []
    for grp in available_groups:
        group_tuples.append((grp.templates.order_by('-date')[0].date, grp))
    group_tuples.sort(key=lambda gt: gt[0], reverse=True)
    template_types = [t for t in Template.TEMPLATE_TYPES]
    can_order_pool = show_user == "my"
    new_pool_possible = True
    display_legend = False
    for pool in pools:
        if not pool.finished:
            display_legend = True
    per_pool_quota = None
    pools_remaining = None
    num_user_vms = Appliance.objects.filter(appliance_pool__owner=request.user).count()
    if request.user.has_quotas:
        if request.user.quotas.total_pool_quota is not None:
            if request.user.quotas.total_pool_quota <= len(pools):
                new_pool_possible = False
            pools_remaining = request.user.quotas.total_pool_quota - len(pools)
        if request.user.quotas.total_vm_quota is not None:
            if request.user.quotas.total_vm_quota <= num_user_vms:
                new_pool_possible = False
        if request.user.quotas.per_pool_quota is not None:
            per_pool_quota = request.user.quotas.per_pool_quota
            remaining_vms = request.user.quotas.total_vm_quota - num_user_vms
            if remaining_vms < per_pool_quota:
                per_pool_quota = remaining_vms
    per_pool_quota_enabled = per_pool_quota is not None
    can_change_hw = request.user.has_perm('appliances.can_modify_hw')
    provider_types = Provider.get_available_provider_types(request.user)
    return render(request, 'appliances/my_appliances.html', locals())

def can_operate_appliance_or_pool(appliance_or_pool, user):
    if user.is_superuser:
        return True
    else:
        return appliance_or_pool.owner == user

@only_authenticated
def appliance_action(request, appliance_id, action, x=None):
    try:
        appliance = Appliance.objects.get(id=appliance_id)
    except ObjectDoesNotExist:
        messages.warning(request, 'Appliance with ID {} does not exist!.'.format(appliance_id))
        return go_back_or_home(request)
    if not can_operate_appliance_or_pool(appliance, request.user):
        messages.warning(request, 'This appliance belongs either to some other user or nobody.')
        return go_back_or_home(request)
    if action == "start":
        if appliance.power_state != Appliance.Power.ON:
            chain(
                appliance_power_on.si(appliance.id),
                (wait_appliance_ready if appliance.preconfigured else mark_appliance_ready).si(
                    appliance.id))()
            messages.success(request, 'Initiated launch of appliance.')
            return go_back_or_home(request)
        else:
            messages.info(request, 'Appliance was already powered on.')
            return go_back_or_home(request)
    elif action == "reboot":
        if appliance.power_state == Appliance.Power.ON:
            chain(
                appliance_reboot.si(appliance.id),
                mark_appliance_ready.si(appliance.id))()
            messages.success(request, 'Initiated reboot of appliance.')
            return go_back_or_home(request)
        else:
            messages.warning(request, 'Only powered on appliances can be rebooted')
            return go_back_or_home(request)
    elif action == "stop":
        if appliance.power_state != Appliance.Power.OFF:
            appliance_power_off.delay(appliance.id)
            messages.success(request, 'Initiated stop of appliance.')
            return go_back_or_home(request)
        else:
            messages.info(request, 'Appliance was already powered off.')
            return go_back_or_home(request)
    elif action == "suspend":
        if appliance.power_state != Appliance.Power.SUSPENDED:
            appliance_suspend.delay(appliance.id)
            messages.success(request, 'Initiated suspend of appliance.')
            return go_back_or_home(request)
        else:
            messages.info(request, 'Appliance was already suspended.')
            return go_back_or_home(request)
    elif action == "kill":
        Appliance.kill(appliance)
        messages.success(request, 'Kill initiated.')
        return go_back_or_home(request)
    elif action == "dont_expire":
        if not request.user.is_superuser:
            messages.warning(request, 'Disabling expiration time is allowed only for superusers.')
            return go_back_or_home(request)
        with transaction.atomic():
            appliance.leased_until = None
            appliance.save()
        messages.success(request, 'Lease disabled successfully. Be careful.')
        return go_back_or_home(request)
    elif action == "set_lease":
        if not can_operate_appliance_or_pool(appliance, request.user):
            messages.warning(request, 'This appliance belongs either to some other user or nobody.')
            return go_back_or_home(request)
        appliance.prolong_lease(time=int(x))
        messages.success(request, 'Lease prolonged successfully.')
        return go_back_or_home(request)
    else:
        messages.warning(request, "Unknown action '{}'".format(action))

@only_authenticated
def prolong_lease_pool(request, pool_id, minutes):
    try:
        appliance_pool = AppliancePool.objects.get(id=pool_id)
    except ObjectDoesNotExist:
        messages.warning(request, 'Appliance pool with ID {} does not exist!.'.format(pool_id))
        return go_back_or_home(request)
    if not can_operate_appliance_or_pool(appliance_pool, request.user):
        messages.warning(request, 'This appliance belongs either to some other user or nobody.')
        return go_back_or_home(request)
    appliance_pool.prolong_lease(time=int(minutes))
    messages.success(request, 'Lease prolonged successfully.')
    return go_back_or_home(request)

@only_authenticated
def dont_expire_pool(request, pool_id):
    try:
        appliance_pool = AppliancePool.objects.get(id=pool_id)
    except ObjectDoesNotExist:
        messages.warning(request, 'Pool with ID {} does not exist!.'.format(pool_id))
        return go_back_or_home(request)
    if not request.user.is_superuser:
        messages.warning(request, 'Disabling expiration time is allowed only for superusers.')
        return go_back_or_home(request)
    with transaction.atomic():
        for appliance in appliance_pool.appliances:
            appliance.leased_until = None
            appliance.save()
    messages.success(request, 'Lease disabled successfully. Be careful.')
    return go_back_or_home(request)

@only_authenticated
def kill_pool(request, pool_id):
    try:
        pool = AppliancePool.objects.get(id=pool_id)
    except ObjectDoesNotExist:
        messages.warning(request, 'Pool with ID {} does not exist!.'.format(pool_id))
        return go_back_or_home(request)
    if not can_operate_appliance_or_pool(pool, request.user):
        messages.warning(request, 'This pool belongs either to some other user or nobody.')
        return go_back_or_home(request)
    try:
        pool.kill()
    except Exception as e:
        messages.warning(request, "Exception {}: {}".format(type(e).__name__, str(e)))
    else:
        messages.success(request, 'Kill successfully initiated.')
    return go_back_or_home(request)

@only_authenticated
def kill_all_pools(request, user_id):
    try:
        user = User.objects.get(id=user_id)
    except ObjectDoesNotExist:
        messages.warning(request, 'User with ID {} does not exist!.'.format(user_id))
        return go_back_or_home(request)
    if user != request.user and not (user.is_staff or user.is_superuser):
        messages.warning(request, 'You cannot operate other users pools.')
        return go_back_or_home(request)
    for pool in AppliancePool.objects.filter(owner=user):
        pool_id = pool.id
        try:
            pool.kill()
        except Exception as e:
            messages.warning(
                request, "Exception during pool {} kill: {}: {}".format(
                    pool_id, type(e).__name__, str(e)))
        else:
            messages.success(request, 'Kill of pool {} successfully initiated'.format(pool_id))
    return go_back_or_home(request)

@only_authenticated
def delete_pool(request, pool_id):
    try:
        pool = AppliancePool.objects.get(id=pool_id)
    except ObjectDoesNotExist:
        messages.warning(request, 'Pool with ID {} does not exist!.'.format(pool_id))
        return go_back_or_home(request)
    if not can_operate_appliance_or_pool(pool, request.user):
        messages.warning(request, 'This pool belongs either to some other user or nobody.')
        return go_back_or_home(request)
    try:
        pool.delete()
    except Exception as e:
        messages.warning(request, "Exception {}: {}".format(type(e).__name__, str(e)))
    else:
        messages.success(request, 'Deleted.')
    return go_back_or_home(request)

def set_pool_description(request):
    if not request.user.is_authenticated():
        raise PermissionDenied()
    try:
        pool_id = request.POST.get("pool_id")
        pool = AppliancePool.objects.get(id=pool_id)
    except ObjectDoesNotExist:
        raise Http404('Pool with ID {} does not exist!.'.format(pool_id))
    if not can_operate_appliance_or_pool(pool, request.user):
        raise PermissionDenied()
    description = request.POST.get("description")
    pool.description = description
    pool.save()
    return HttpResponse("")

def delete_template_provider(request):
    if not request.user.is_authenticated() or not request.user.is_superuser:
        return HttpResponseForbidden("Only authenticated superusers can operate this action.")
    template_id = request.POST["template_id"]
    try:
        template = Template.objects.get(id=template_id)
    except ObjectDoesNotExist:
        raise Http404('Template with ID {} does not exist!.'.format(template_id))
    if not request.user.is_superuser:
        return HttpResponseForbidden("Only superusers can operate this action.")
    task = delete_template_from_provider.delay(template.id)
    return HttpResponse(task.id)

@only_authenticated
def clone_pool(request):
    try:
        count = int(request.POST["count"])
        source_pool_id = int(request.POST["source_pool_id"])
        pool = AppliancePool.objects.get(id=source_pool_id)
        result_pool = pool.clone(num_appliances=count, owner=request.user)
        messages.success(request, "Pool cloned - id {}".format(result_pool.id))
    except Exception as e:
        messages.warning(request, "{}: {}".format(type(e).__name__, e))
    return go_back_or_home(request)

@only_authenticated
def request_pool(request):
    try:
        group = request.POST["stream"]
        version = request.POST["version"]
        template_type = request.POST["template_type"]
        if version == "latest":
            version = None
        date = request.POST["date"]
        if date == "latest":
            date = None
        provider = request.POST["provider"]
        if provider == "any":
            provider = None
        preconfigured = request.POST.get("preconfigured", "false").lower() == "true"
        yum_update = request.POST.get("yum_update", "false").lower() == "true"
        provider_type = request.POST.get("provider_type", "any").lower()
        if not provider_type or provider_type == 'any':
            provider_type = None

        count = int(request.POST["count"])
        lease_time = int(request.POST.get("expiration", 60))
        ram = None
        cpu = None
        if request.user.has_perm('appliances.can_modify_hw'):
            if 'ram' in request.POST:
                ram = int(request.POST['ram'])
            if 'cpu' in request.POST:
                cpu = int(request.POST['cpu'])

        pool_id = AppliancePool.create(
            request.user, group, version, date, provider, count, lease_time, preconfigured,
            yum_update, ram, cpu, provider_type, template_type).id
        messages.success(request, "Pool requested - id {}".format(pool_id))
    except Exception as e:
        messages.warning(request, "{}: {}".format(type(e).__name__, e))
    return go_back_or_home(request)

@only_authenticated
def transfer_pool(request):
    try:
        pool_id = int(request.POST["pool_id"])
        user_id = int(request.POST["user_id"])
        with transaction.atomic():
            pool = AppliancePool.objects.get(id=pool_id)
            if not request.user.is_superuser:
                if pool.owner != request.user:
                    raise Exception("User does not have the right to change this pool's owner!")
            user = User.objects.get(id=user_id)
            if user == request.user:
                raise Exception("Why changing owner back to yourself? That does not make sense!")
            # original_owner = pool.owner
            pool.owner = user
            pool.save()
        # Rename appliances
        # for appliance in pool.appliances:
        #     if appliance.name.startswith("{}_".format(original_owner.username)):
        #         # Change name
        #         appliance_rename.delay(
        #             appliance.id, user.username + appliance.name[len(original_owner.username):])
    except Exception as e:
        messages.warning(request, "Exception {} happened: {}".format(type(e).__name__, str(e)))
    else:
        messages.success(request, "Success!")
    finally:
        return go_back_or_home(request)

def vms(request, current_provider=None):
    if not request.user.is_authenticated() or not request.user.is_superuser:
        return go_home(request)
    all_provider_keys = sorted(Provider.get_available_provider_keys())
    providers = []
    provider_keys = []
    if request.user.is_staff or request.user.is_superuser:
        user_filter = {}
    else:
        user_filter = {'user_groups__in': request.user.groups.all()}
    for provider_key in all_provider_keys:
        try:
            provider = Provider.objects.filter(id=provider_key, **user_filter).distinct().first()
        except ObjectDoesNotExist:
            continue
        if provider is not None:
            providers.append((provider_key, provider.is_working))
            provider_keys.append(provider_key)
    if current_provider is None and providers:
        return redirect("vms_at_provider", current_provider=provider_keys[0])
    return render(request, 'appliances/vms/index.html', locals())

def vms_table(request, current_provider=None):
    if not request.user.is_authenticated() or not request.user.is_superuser:
        return go_home(request)
    try:
        manager = get_mgmt(current_provider)
        vms = sorted([vm.name for vm in manager.list_vms()])
        return render(request, 'appliances/vms/_list.html', locals())
    except Exception as e:
        return HttpResponse('{}: {}'.format(type(e).__name__, str(e)), content_type="text/plain")

def power_state(request, current_provider):
    if not request.user.is_authenticated() or not request.user.is_superuser:
        return go_home(request)
    vm_name = request.POST["vm_name"]
    manager = get_mgmt(current_provider)
    # TODO: change after openshift wrapanapi refactor
    if isinstance(manager, Openshift):
        state = manager.vm_status(vm_name)
    else:
        state = manager.get_vm(vm_name).state
    state = Appliance.POWER_STATES_MAPPING.get(state, "unknown")
    return HttpResponse(state, content_type="text/plain")

def power_state_buttons(request, current_provider):
    if not request.user.is_authenticated() or not request.user.is_superuser:
        return go_home(request)
    manager = get_mgmt(current_provider)
    vm_name = request.POST["vm_name"]
    power_state = request.POST["power_state"]
    can_power_on = power_state in {Appliance.Power.SUSPENDED, Appliance.Power.OFF}
    can_power_off = power_state in {Appliance.Power.ON}
    can_suspend = power_state in {Appliance.Power.ON} and manager.can_suspend
    can_delete = power_state in {Appliance.Power.OFF}
    return render(request, 'appliances/vms/_buttons.html', locals())

def vm_action(request, current_provider):
    if not request.user.is_authenticated() or not request.user.is_superuser:
        return HttpResponse("Not authenticated", content_type="text/plain")
    try:
        get_mgmt(current_provider)
    except Exception as e:
        return HttpResponse(
            "Troubles with provider {}: {}".format(current_provider, str(e)),
            content_type="text/plain")
    vm_name = request.POST["vm_name"]
    action = request.POST["action"]
    if action == "poweron":
        anyvm_power_on.delay(current_provider, vm_name)
    elif action == "poweroff":
        anyvm_power_off.delay(current_provider, vm_name)
    elif action == "suspend":
        anyvm_suspend.delay(current_provider, vm_name)
    elif action == "delete":
        anyvm_delete.delay(current_provider, vm_name)
    else:
        HttpResponse("No such action {}!".format(action), content_type="text/plain")
    logger().info("User {} initiated {} on {}@{}".format(
        request.user.username, action, vm_name, current_provider))
    return HttpResponse("Action {} was initiated".format(action), content_type="text/plain")

def logger():
    return create_logger("sprout_vm_actions")

def rename_appliance(request):
    post = json.loads(request.body)
    if not request.user.is_authenticated():
        raise PermissionDenied()
    try:
        appliance_id = post.get("appliance_id")
        appliance = Appliance.objects.get(id=appliance_id)
    except ObjectDoesNotExist:
        raise Http404('Appliance with ID {} does not exist!.'.format(appliance_id))
    if not can_operate_appliance_or_pool(appliance, request.user):
        raise PermissionDenied("Permission denied")
    new_name = post.get("new_name")
    return HttpResponse(str(appliance_rename.delay(appliance.id, new_name).task_id))

def set_appliance_description(request):
    if request.method != 'POST':
        messages.error(request, "Invalid request.")
        return go_home(request)
    post = json.loads(request.body)
    if not request.user.is_authenticated():
        raise PermissionDenied()
    try:
        appliance_id = post.get("appliance_id")
        appliance = Appliance.objects.get(id=appliance_id)
    except ObjectDoesNotExist:
        raise Http404('Appliance with ID {} does not exist!.'.format(appliance_id))
    if not can_operate_appliance_or_pool(appliance, request.user):
        raise PermissionDenied("Permission denied")
    new_description = post.get("description")
    appliance.description = new_description
    appliance.save()
    return json_response(True)

def task_result(request):
    post = json.loads(request.body)
    task_id = post.get("task_id")
    result = AsyncResult(task_id)
    if not result.ready():
        return json_response(None)
    return json_response(result.get(timeout=1))

def provider_enable_disable(request, provider_id, disabled=None):
    if not request.user.is_authenticated():
        return go_home(request)
    try:
        provider = Provider.objects.get(id=provider_id)
    except ObjectDoesNotExist:
        messages.warning(request, 'Provider with ID {} does not exist!.'.format(provider_id))
        return go_back_or_home(request)
    if not request.user.is_superuser:
        messages.warning(request, 'Providers can be modified only by superusers.')
        return go_back_or_home(request)
    provider.disabled = disabled
    provider.save()
    messages.success(
        request, 'Provider {}, {}.'.format(provider_id, "disabled" if disabled else "enabled"))
    return go_back_or_home(request)

def check_appliance(request, provider_id, appliance_name):
    try:
        appliance = Appliance.objects.get(name=appliance_name, template__provider=provider_id)
    except ObjectDoesNotExist:
        return json_response(None)
    owner = appliance.owner
    if owner is not None:
        owner = owner.username
    data = {
        'stream': appliance.template.template_group.id,
        'version': appliance.template.version,
        'date': appliance.template.date.strftime('%Y-%m-%d'),
        'preconfigured': appliance.template.preconfigured,
        'owner': owner,
    }
    return json_response(data)

def check_template(request, provider_id, template_name):
    try:
        template = Template.objects.get(name=template_name, provider=provider_id)
    except ObjectDoesNotExist:
        return json_response(None)
    data = {
        'stream': template.template_group.id,
        'version': template.version,
        'date': template.date.strftime('%Y-%m-%d'),
        'preconfigured': template.preconfigured,
    }
    return json_response(data)

def check_pool(request, pool_id):
    try:
        pool = AppliancePool.objects.get(id=pool_id)
    except ObjectDoesNotExist:
        return json_response(None)
    data = {
        'description': pool.description,
        'stream': pool.group.id,
        'version': pool.version,
        'date': pool.date.strftime('%Y-%m-%d'),
        'preconfigured': pool.preconfigured,
        'finished': pool.finished,
        'owner': pool.owner.username,
        'appliances': [[a.name, a.template.provider.id] for a in pool.appliances]
    }
    return json_response(data)

def check_pools(request):
    data = []
    for pool in AppliancePool.objects.all():
        pool_data = {
            'description': pool.description,
            'id': pool.id,
            'stream': pool.group.id,
            'version': pool.version,
            'date': pool.date.strftime('%Y-%m-%d'),
            'preconfigured': pool.preconfigured,
            'finished': pool.finished,
            'owner': pool.owner.username,
            'appliances': [[a.name, a.template.provider.id] for a in pool.appliances]
        }
        data.append(pool_data)
    return json_response(data)

def view_bug_query(request, query_id):
    if not request.user.is_authenticated():
        return go_home(request)
    queries = BugQuery.visible_for_user(request.user)
    query = BugQuery.objects.get(id=query_id)
    if query.owner is not None and query.owner != request.user:
        if not request.user.is_superuser:
            messages.info(request, "You cannot view BugQuery {}.".format(query.id))
            return go_home(request)
    try:
        bugs = query.list_bugs(request.user)
    except RPCFault as e:
        messages.error(request, 'Bugzilla query error {}: {}'.format(e.faultCode, e.faultString))
        return go_home(request)
    return render(request, 'bugs/list_query.html', locals())

def view_bug_queries(request):
    if not request.user.is_authenticated():
        return go_home(request)
    try:
        first_query = BugQuery.visible_for_user(request.user)[0]
    except IndexError:
        first_query = None
    if first_query is not None:
        return redirect('view_bug_query', first_query.id)
    else:
        # No Group
        messages.info(request, "No query present, redirected to the homepage.")
        return go_home(request)

def new_bug_query(request):
    if not request.user.is_authenticated():
        return go_home(request)
    queries = BugQuery.visible_for_user(request.user)
    query = None
    if request.method == 'GET':
        return render(request, 'bugs/new_query.html', locals())
    elif request.method != 'POST':
        messages.error(request, "Invalid request.")
        return go_home(request)
    # Create a new one
    name = request.POST['name']
    url = request.POST['url']
    global_ = request.POST.get('global', 'false') == 'true'
    if not request.user.is_superuser:
        global_ = False
    if global_:
        owner = None
    else:
        owner = request.user
    bug_query = BugQuery(name=name, url=url, owner=owner)
    bug_query.save()
    messages.info(request, "Query with name {} added.".format(name))
    return redirect('view_bug_query', bug_query.id)

def delete_bug_query(request, query_id):
    if not request.user.is_authenticated():
        return go_home(request)
    query = BugQuery.objects.get(id=query_id)
    if query.owner == request.user or request.user.is_superuser:
        query.delete()
        messages.info(request, "Query with name {} deleted.".format(query.name))
        return redirect('view_bug_queries')
    else:
        messages.error(request, "You cannot delete query with name {}.".format(query.name))
        return redirect('view_bug_queries')

def check_query(request):
    if not request.user.is_authenticated():
        return go_home(request)
    if request.method != 'POST':
        return HttpResponseForbidden('Only POST allowed')
    bz = Bugzilla.from_config().bugzilla
    try:
        parsed = bz.url_to_query(request.POST['url'])
        if not parsed:
            parsed = None
    except Exception:
        parsed = None
    if 'cmdtype' in parsed:
        # It is a command and that is not supported within .query()
        parsed = None
    return json_response(parsed)

def swap_offenders(request):
    appliances = Appliance.objects.filter(
        power_state=Appliance.Power.ON).exclude(Q(swap=None) | Q(swap=0)).order_by('-swap')[:15]
    failed_ssh = Appliance.objects.filter(ssh_failed=True, power_state=Appliance.Power.ON).order_by(
        'appliance_pool__owner__username', 'name')
    return render(request, 'appliances/swap_offenders.html', locals())

def template_configurations(request):
    if not request.user.is_superuser or not request.user.is_staff:
        messages.info(request, 'You do not have the right to see the template configuration view')
        return go_back_or_home(request)
    templates_configuring = Template.objects\
        .select_related('provider', 'template_group')\
        .filter(ready=False, preconfigured=True)
    return render(request, 'appliances/template_conf.html', locals())

@only_authenticated
def nuke_template(request):
    if not request.user.is_superuser:
        return HttpResponseForbidden("Only authenticated superusers can operate this action.")
    if request.method != 'POST':
        return HttpResponseForbidden('Only POST allowed')


# 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 django.http import Http404
from django.test.client import RequestFactory
from django.test.utils import override_settings

from bedrock.base.urlresolvers import reverse
from mock import patch, Mock
from nose.tools import eq_
from pathlib import Path
from pyquery import PyQuery as pq
from rna.models import Release

from bedrock.firefox.firefox_details import FirefoxDesktop
from bedrock.mozorg.tests import TestCase
from bedrock.releasenotes import views
from bedrock.thunderbird.details import ThunderbirdDesktop

DATA_PATH = str(Path(__file__).parent / 'data')
firefox_desktop = FirefoxDesktop(json_dir=DATA_PATH)
thunderbird_desktop = ThunderbirdDesktop(json_dir=DATA_PATH)

class TestRNAViews(TestCase):
    def setUp(self):
        self.factory = RequestFactory()
        self.request = self.factory.get('/')

        self.render_patch = patch('bedrock.releasenotes.views.l10n_utils.render')
        self.mock_render = self.render_patch.start()
        self.mock_render.return_value.has_header.return_value = False

    def tearDown(self):
        self.render_patch.stop()

    @property
    def last_ctx(self):
        """
        Convenient way to access the context of the last rendered
        response.
        """
        return self.mock_render.call_args[0][2]

    @patch('bedrock.releasenotes.views.get_object_or_404')
    @patch('bedrock.releasenotes.views.Q')
    def test_get_release_or_404(self, Q, get_object_or_404):
        eq_(views.get_release_or_404('version', 'product'),
            get_object_or_404.return_value)
        get_object_or_404.assert_called_with(
            Release, Q.return_value, version='version')
        Q.assert_called_with(product='product')

    @patch('bedrock.releasenotes.views.get_object_or_404')
    @patch('bedrock.releasenotes.views.Q')
    def test_get_release_or_404_esr(self, Q, get_object_or_404):
        eq_(views.get_release_or_404('24.5.0', 'Firefox'),
            get_object_or_404.return_value)
        Q.assert_any_call(product='Firefox')
        Q.assert_any_call(product='Firefox Extended Support Release')
        Q.__or__.assert_called()

    @override_settings(DEV=False)
    @patch('bedrock.releasenotes.views.release_notes_template')
    @patch('bedrock.releasenotes.views.get_release_or_404')
    @patch('bedrock.releasenotes.views.equivalent_release_url')
    def test_release_notes(self, mock_equiv_rel_url, get_release_or_404,
                           mock_release_notes_template):
        """
        Should use release returned from get_release_or_404 with the
        correct params and pass the correct context variables and
        template to l10n_utils.render.
        """
        mock_release = get_release_or_404.return_value
        mock_release.major_version.return_value = '34'
        mock_release.notes.return_value = ([Release(id=1), Release(id=2)],
                                           [Release(id=3), Release(id=4)])

        views.release_notes(self.request, '27.0')
        get_release_or_404.assert_called_with('27.0', 'Firefox')
        mock_release.notes.assert_called_with(public_only=True)
        eq_(self.last_ctx['version'], '27.0')
        eq_(self.last_ctx['release'], mock_release)
        eq_(self.last_ctx['new_features'], [Release(id=1), Release(id=2)])
        eq_(self.last_ctx['known_issues'], [Release(id=3), Release(id=4)])
        eq_(self.mock_render.call_args[0][1],
            mock_release_notes_template.return_value)
        mock_equiv_rel_url.assert_called_with(mock_release)
        mock_release_notes_template.assert_called_with(
            mock_release.channel, 'Firefox', 34)

    @patch('bedrock.releasenotes.views.get_release_or_404')
    @patch('bedrock.releasenotes.views.releasenotes_url')
    def test_release_notes_beta_redirect(self, releasenotes_url,
                                         get_release_or_404):
        """
        Should redirect to url for beta release
        """
        get_release_or_404.side_effect = [Http404, 'mock release']
        releasenotes_url.return_value = '/firefox/27.0beta/releasenotes/'
        response = views.release_notes(self.request, '27.0')
        eq_(response.status_code, 302)
        eq_(response['location'], '/firefox/27.0beta/releasenotes/')
        get_release_or_404.assert_called_with('27.0beta', 'Firefox')
        releasenotes_url.assert_called_with('mock release')

    @patch('bedrock.releasenotes.views.get_release_or_404')
    def test_system_requirements(self, get_release_or_404):
        """
        Should use release returned from get_release_or_404, with a
        default channel of Release and default product of Firefox,
        and pass the version to l10n_utils.render
        """
        views.system_requirements(self.request, '27.0.1')
        get_release_or_404.assert_called_with('27.0.1', 'Firefox')
        eq_(self.last_ctx['release'], get_release_or_404.return_value)
        eq_(self.last_ctx['version'], '27.0.1')
        eq_(self.mock_render.call_args[0][1],
            'firefox/releases/system_requirements.html')

    def test_release_notes_template(self):
        """
        Should return correct template name based on channel
        and product
        """
        eq_(views.release_notes_template('Nightly', 'Firefox'),
            'firefox/releases/nightly-notes.html')
        eq_(views.release_notes_template('Aurora', 'Firefox'),
            'firefox/releases/aurora-notes.html')
        eq_(views.release_notes_template('Aurora', 'Firefox', 35),
            'firefox/releases/dev-browser-notes.html')
        eq_(views.release_notes_template('Aurora', 'Firefox', 34),
            'firefox/releases/aurora-notes.html')
        eq_(views.release_notes_template('Beta', 'Firefox'),
            'firefox/releases/beta-notes.html')
        eq_(views.release_notes_template('Release', 'Firefox'),
            'firefox/releases/release-notes.html')
        eq_(views.release_notes_template('ESR', 'Firefox'),
            'firefox/releases/esr-notes.html')
        eq_(views.release_notes_template('Release', 'Thunderbird'),
            'thunderbird/releases/release-notes.html')
        eq_(views.release_notes_template('Beta', 'Thunderbird'),
            'thunderbird/releases/beta-notes.html')
        eq_(views.release_notes_template('', ''),
            'firefox/releases/release-notes.html')

    @override_settings(DEV=False)
    @patch('bedrock.releasenotes.views.get_object_or_404')
    def test_non_public_release(self, get_object_or_404):
        """
        Should raise 404 if not release.is_public and not settings.DEV
        """
        get_object_or_404.return_value = Release(is_public=False)
        with self.assertRaises(Http404):
            views.get_release_or_404('42', 'Firefox')

    @patch('bedrock.releasenotes.views.releasenotes_url')
    def test_no_equivalent_release_url(self, mock_releasenotes_url):
        """
        Should return None without calling releasenotes_url
        """
        release = Mock()
        release.equivalent_android_release.return_value = None
        release.equivalent_desktop_release.return_value = None
        eq_(views.equivalent_release_url(release), None)
        eq_(mock_releasenotes_url.called, 0)

    @patch('bedrock.releasenotes.views.releasenotes_url')
    def test_android_equivalent_release_url(self, mock_releasenotes_url):
        """
        Should return the url for the equivalent android release
        """
        release = Mock()
        eq_(views.equivalent_release_url(release),
            mock_releasenotes_url.return_value)
        mock_releasenotes_url.assert_called_with(
            release.equivalent_android_release.return_value)

    @patch('bedrock.releasenotes.views.releasenotes_url')
    def test_desktop_equivalent_release_url(self, mock_releasenotes_url):
        """
        Should return the url for the equivalent desktop release
        """
        release = Mock()
        release.equivalent_android_release.return_value = None
        eq_(views.equivalent_release_url(release),
            mock_releasenotes_url.return_value)
        mock_releasenotes_url.assert_called_with(
            release.equivalent_desktop_release.return_value)

    @patch('bedrock.releasenotes.views.android_builds')
    def test_get_download_url_android(self, mock_android_builds):
        """
        Shoud return the download link for the release.channel from
        android_builds
        """
        mock_android_builds.return_value = [{'download_link': '/download'}]
        release = Mock(product='Firefox for Android')
        link = views.get_download_url(release)
        eq_(link, '/download')
        mock_android_builds.assert_called_with(release.channel)

    def test_get_download_url_thunderbird(self):
        release = Mock(product='Thunderbird')