import tkinter
from tkinter import ttk
from tkinter import filedialog
import os
import nanoplot.utils as utils
import nanoplot.NanoPlot as nanoplot
import nanocomp.NanoComp as nanocomp
from nanoget import get_input
from nanoplotter import check_valid_format
from nanomath import write_stats
import logging
import webbrowser
# import subprocess
# import shlex

class Dataset(object):
    def __init__(self, path, name):
        self.path = path
        self.name = name

# class nanoFiltGui(tkinter.Frame):
#     def __init__(self, logfile):
#         self.logfile = logfile
#         s = ttk.Style()
#         if 'alt' in s.theme_names():
#             s.theme_use('alt')
#         self.root = tkinter.Frame.__init__(self)
#         self.master.title("Welcome to NanoFilt")
#         self.master.rowconfigure(5, weight=1)
#         self.master.columnconfigure(5, weight=1)
#         self.grid(sticky=tkinter.W + tkinter.E + tkinter.N + tkinter.S, padx=10, pady=10)
#         self.sourcefastq = tkinter.StringVar()
#         self.destfastq = tkinter.StringVar()
#         self.minlength = tkinter.IntVar()
#         self.minqual = tkinter.IntVar()
#         self.minGC = tkinter.IntVar()
#         self.maxGC = tkinter.IntVar()
#         self.filebutton = ttk.Button(self,
#                                      text='Select input fastq.gz file',
#                                      command=self.select_file_and_add
#                                      )
#         self.filebutton.grid(column=0, row=2, sticky=tkinter.W, padx=3, pady=3)
#         vcmd = (self.register(self.validate_integer),
#                 '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
#
#         ttk.Label(self, text="Minimal read length?"
#                   ).grid(column=1, row=2, padx=20)
#         ttk.Entry(self,
#                   textvariable=self.minlength,
#                   validate='key',
#                   validatecommand=vcmd
#                   ).grid(column=2, row=2, sticky=tkinter.W, padx=20, pady=3)
#
#         ttk.Label(self, text="Minimal average basecall quality?"
#                   ).grid(column=1, row=3, padx=20)
#         ttk.Entry(self,
#                   textvariable=self.minqual,
#                   validate='key',
#                   validatecommand=vcmd
#                   ).grid(column=2, row=3, sticky=tkinter.W, padx=20, pady=3)
#
#         ttk.Label(self, text="Minimal GC content?"
#                   ).grid(column=1, row=4, padx=20)
#         ttk.Entry(self,
#                   textvariable=self.minGC,
#                   validate='key',
#                   validatecommand=vcmd
#                   ).grid(column=2, row=4, sticky=tkinter.W, padx=20, pady=3)
#
#         ttk.Label(self, text="Maximal GC content?"
#                   ).grid(column=1, row=5, padx=20)
#         ttk.Entry(self,
#                   textvariable=self.maxGC,
#                   validate='key',
#                   validatecommand=vcmd
#                   ).grid(column=2, row=5, sticky=tkinter.W, padx=20, pady=3)
#
#     def select_file_and_add(self):
#         self.sourcefastq = filedialog.askopenfile(initialdir=os.path.expanduser("~"))
#         if self.sourcefastq:
#             ttk.Label(self, text=os.path.basename(self.sourcefastq.name)[:40]
#                       ).grid(column=0, row=3, sticky=tkinter.W, padx=3, pady=3)
#             ttk.Button(self,
#                        text='Select output fastq.gz',
#                        command=self.select_destfq_and_add
#                        ).grid(column=0, row=4, sticky=tkinter.W, padx=3, pady=3)
#
#     def select_destfq_and_add(self):
#         self.destfastq = filedialog.asksaveasfilename()
#         if self.destfastq:
#             ttk.Label(self, text=self.destfastq
#                       ).grid(column=0, row=5, sticky=tkinter.W, padx=3, pady=3)
#             self.runbutton = ttk.Button(self,
#                                         text="Start filtering",
#                                         command=self.run,
#                                         )
#             self.runbutton.grid(column=0, row=6, sticky=tkinter.W, padx=3, pady=3)
#
#     def validate_integer(self, action, index, value_if_allowed, prior_value,
#                          text, validation_type, trigger_type, widget_name):
#         """Check if text Entry is valid (number).
#
#         I have no idea what all these arguments are doing here but took this from
#         https://stackoverflow.com/questions/8959815/restricting-the-value-in-tkinter-entry-widget
#         """
#         if(action == '1'):
#             if text in '0123456789.-+':
#                 try:
#                     int(value_if_allowed)
#                     return True
#                 except ValueError:
#                     return False
#             else:
#                 return False
#         else:
#             return True
#
#     def run(self):
#         """gunzip -c input.fastq.gz | \
#            NanoFilt -q Q -l L --minGC iGC --maxGC aGC | \
#            gzip > output.fastq.gz
#         """
#         inp = subprocess.Popen(shlex.split("gunzip -c " + self.sourcefastq.name),
#                                stdout=subprocess.PIPE)
#         cmd = "NanoFilt "
#
#         minqual = self.minqual.get()
#         if minqual:
#             cmd += "-q " + str(minqual) + " "
#
#         minlength = self.minlength.get()
#         if minlength:
#             cmd += "-l " + str(minlength) + " "
#
#         minGC = self.minGC.get()
#         if minGC:
#             cmd += "--minGC " + str(minGC) + " "
#
#         maxGC = self.maxGC.get()
#         if maxGC:
#             cmd += "--maxGC " + str(maxGC) + " "
#
#         filt = subprocess.check_output(shlex.split(cmd),
#                                        stdin=inp.stdout,
#                                        stdout=subprocess.PIPE)
#         subprocess.check_output(shlex.split("gzip"),
#                                 stdin=filt.stdout,
#                                 stdout=open(self.destfastq, 'wb'))

class nanoCompGui(tkinter.Frame):
    def __init__(self, logfile):
        self.logfile = logfile
        s = ttk.Style()
        if 'alt' in s.theme_names():
            s.theme_use('alt')
        self.root = tkinter.Frame.__init__(self)
        self.master.title("Welcome to NanoComp")
        self.master.rowconfigure(5, weight=1)
        self.master.columnconfigure(5, weight=1)
        self.grid(sticky=tkinter.W + tkinter.E + tkinter.N + tkinter.S, padx=10, pady=10)
        self.source = tkinter.StringVar()
        self.figformat = tkinter.StringVar(None, "png")
        self.plot = tkinter.StringVar(None, "violin")
        self.readtype = tkinter.StringVar(None, "1D")
        self.targetfile_list = list()
        ttk.Label(self,
                  text='Which data type do you want to analyze?',
                  ).grid(column=0, row=0)
        sources = {"summary": 'Summary file', "fastq_rich": 'Fastq file(s)', "bam": 'Bam file'}
        for i, source in enumerate(sources.keys()):
            ttk.Radiobutton(self,
                            text=sources[source],
                            variable=self.source,
                            value=source,
                            command=self.selected_source
                            ).grid(column=0, row=1 + i, sticky=tkinter.W, padx=3, pady=3)
        ttk.Label(self, text="Which type of sequencing?").grid(
            column=1, row=0, padx=75, pady=3)
        self.readtypes = []
        for i, t in enumerate(['1D', '2D', '1D2']):
            a = ttk.Radiobutton(self,
                                text=t,
                                variable=self.readtype,
                                value=t,
                                state=tkinter.DISABLED)
            a.grid(column=1, row=1 + i, sticky=tkinter.W, padx=75, pady=3)
            self.readtypes.append(a)

    def select_file_and_add(self):
        self.targetfile = filedialog.askopenfile(initialdir=os.path.expanduser("~"))
        if self.targetfile:
            self.targetfile_list.append(
                Dataset(path=self.targetfile.name,
                        name=tkinter.StringVar(None, os.path.basename(self.targetfile.name)[:40])))
            for i, t in enumerate(self.targetfile_list):
                ttk.Label(self, text=t.name.get()).grid(
                    column=0, row=5 + i, sticky=tkinter.W, padx=3, pady=3)
            self.filebutton.configure(text='Select another data file')
            if len(self.targetfile_list) > 1:
                ttk.Button(self,
                           text='Select output directory',
                           command=self.select_destdir_and_add
                           ).grid(column=1, row=4, sticky=tkinter.W, padx=3, pady=3)

    def select_destdir_and_add(self):
        self.destdir = filedialog.askdirectory()
        if self.destdir:
            ttk.Label(self, text=self.destdir
                      ).grid(column=1, row=5, sticky=tkinter.W, padx=3, pady=3)
            self.runbutton = ttk.Button(self,
                                        text="Start plotting",
                                        command=self.run,
                                        )
            self.runbutton.grid(column=1, row=6, sticky=tkinter.W, padx=3, pady=3)
            ttk.Button(self,
                       text="More Options",
                       command=self.flick_more_options,
                       ).grid(column=1, row=7, sticky=tkinter.W, padx=3, pady=3)

    def selected_source(self):
        if self.source.get() == "summary":
            for i in self.readtypes:
                i.config(state=tkinter.NORMAL)
        else:
            for i in self.readtypes:
                i.config(state=tkinter.DISABLED)
        self.filebutton = ttk.Button(self,
                                     text='Select data file',
                                     command=self.select_file_and_add
                                     )
        self.filebutton.grid(column=0, row=4, sticky=tkinter.W, padx=3, pady=3)

    def flick_more_options(self):
        if not hasattr(self, "optframe"):
            self.optframe = ttk.Frame(self.root)
            self.optframe.grid(column=0, row=14, sticky=tkinter.W + tkinter.N)
            self.left_optframe = ttk.Frame(self.optframe)
            self.left_optframe.grid(column=0, row=0, sticky=tkinter.W + tkinter.N, padx=10)
            ttk.Label(self.left_optframe, text="Which output format?"
                      ).grid(column=0, row=0)
            for i, t in enumerate(['png', 'svg', 'pdf', 'jpeg', 'tif', 'eps']):
                ttk.Radiobutton(self.left_optframe,
                                text=t,
                                variable=self.figformat,
                                value=t,
                                ).grid(column=0, row=1 + i, sticky=tkinter.W)
            self.right_optframe = ttk.Frame(self.optframe)
            self.right_optframe.grid(column=1, row=0, sticky=tkinter.W + tkinter.N, padx=10)
            ttk.Label(self.right_optframe, text="Which plot type?"
                      ).grid(column=0, row=0, sticky=tkinter.W)
            for i, t in enumerate(['violin', 'box']):
                ttk.Radiobutton(self.right_optframe,
                                text=t,
                                variable=self.plot,
                                value=t,
                                ).grid(column=0, row=1 + i, sticky=tkinter.W)
            ttk.Label(self.right_optframe, text="Change textbox to edit display name of dataset."
                      ).grid(column=0, row=3, sticky=tkinter.W)
            for i, t in enumerate(self.targetfile_list):
                e = ttk.Entry(self.right_optframe, textvariable=t.name)
                e.grid(column=0, row=4 + i, sticky=tkinter.W)
        else:
            self.optframe.grid_remove()
            delattr(self, "optframe")

    def run(self):
        self.runbutton.configure(text="Processing... please be patient.")
        self.runbutton.configure(state=tkinter.DISABLED)
        self.update_idletasks()
        try:
            utils.make_output_dir(self.destdir)
            settings = {
                'alength': False,
                "drop_outliers": False,
                "bam": False,
                "summary": False,
                "fastq": False,
                "readtype": self.readtype.get(),
                "format": check_valid_format(self.figformat.get()),
                "plot": self.plot.get(),
                "title": None,
                "path": self.destdir + "/",
                "barcoded": False,
                self.source.get(): True
            }
            datadf = get_input(
                source=self.source.get(),
                files=[f.path for f in self.targetfile_list],
                threads=4,
                readtype=settings["readtype"],
                combine="track",
                names=[f.name.get() for f in self.targetfile_list],
                barcoded=settings["barcoded"])
            identifiers = list(datadf["dataset"].unique())
            write_stats(
                datadfs=[datadf[datadf["dataset"] == i] for i in identifiers],
                outputfile=settings["path"] + "NanoStats.txt",
                names=identifiers)
            plots = nanocomp.make_plots(datadf, settings)
            html_report = nanocomp.make_report(plots, settings["path"], self.logfile)
            logging.info("Finished!")
            self.runbutton.configure(text="Start plotting.")
            self.runbutton.configure(state=tkinter.NORMAL)
            self.update_idletasks()
            webbrowser.open(html_report, new=2)
        except Exception as e:
            logging.error(e, exc_info=True)
            ttk.Label(self, text="Something unexpected happened and NanoPlot has crashed."
                      ).grid(column=1, row=6, sticky=tkinter.W, padx=3, pady=3)
            self.update_idletasks()
            raise

class nanoPlotGui(tkinter.Frame):
    def __init__(self, logfile):
        self.logfile = logfile
        s = ttk.Style()
        if 'alt' in s.theme_names():
            s.theme_use('alt')
        self.root = tkinter.Frame.__init__(self)
        self.master.title("Welcome to NanoPlot")
        self.master.rowconfigure(5, weight=1)
        self.master.columnconfigure(5, weight=1)
        self.grid(sticky=tkinter.W + tkinter.E + tkinter.N + tkinter.S, padx=10, pady=10)
        self.source = tkinter.StringVar()
        self.figformat = tkinter.StringVar(None, "png")
        self.color = tkinter.StringVar(None, "blue")
        self.readtype = tkinter.StringVar(None, "1D")
        self.barcoded = tkinter.BooleanVar(None, False)
        self.loglength = tkinter.BooleanVar(None, False)
        self.n50 = tkinter.BooleanVar(None, False)
        self.maxlength = tkinter.IntVar(None, None)
        self.minqual = tkinter.IntVar(None, None)
        ttk.Label(self,
                  text='Which data type do you want to analyze?',
                  ).grid(column=0, row=0)
        sources = {"summary": 'Summary file', "fastq_rich": 'Fastq file(s)', "bam": 'Bam file'}
        for i, source in enumerate(sources.keys()):
            ttk.Radiobutton(self,
                            text=sources[source],
                            variable=self.source,
                            value=source,
                            command=self.selected_source
                            ).grid(column=0, row=1 + i, sticky=tkinter.W, padx=3, pady=3)
        ttk.Label(self, text="Which type of sequencing?").grid(
            column=1, row=0, padx=75, pady=3)
        self.readtypes = []
        for i, t in enumerate(['1D', '2D', '1D2']):
            a = ttk.Radiobutton(self,
                                text=t,
                                variable=self.readtype,
                                value=t,
                                state=tkinter.DISABLED)
            a.grid(column=1, row=1 + i, sticky=tkinter.W, padx=75, pady=3)
            self.readtypes.append(a)
        ttk.Checkbutton(self,
                        text="Split barcodes",
                        variable=self.barcoded
                        ).grid(column=2, row=2, sticky=tkinter.W, padx=3, pady=3)

    def select_file_and_add(self):
        self.targetfile = filedialog.askopenfile(initialdir=os.path.expanduser("~"))
        if self.targetfile:
            ttk.Label(self, text=os.path.basename(self.targetfile.name)[:40]
                      ).grid(column=1, row=4, sticky=tkinter.W, padx=3, pady=3)
            ttk.Button(self,
                       text='Select output directory',
                       command=self.select_destdir_and_add
                       ).grid(column=0, row=5, sticky=tkinter.W, padx=3, pady=3)

    def select_destdir_and_add(self):
        self.destdir = filedialog.askdirectory()
        if self.destdir:
            ttk.Label(self, text=self.destdir
                      ).grid(column=1, row=5, sticky=tkinter.W, padx=3, pady=3)
            self.runbutton = ttk.Button(self,
                                        text="Start plotting",
                                        command=self.run,
                                        )
            self.runbutton.grid(column=0, row=6, sticky=tkinter.W, padx=3, pady=3)
            ttk.Button(self,
                       text="More Options",
                       command=self.flick_more_options,
                       ).grid(column=0, row=7, sticky=tkinter.W, padx=3, pady=3)

    def selected_source(self):
        if self.source.get() == "summary":
            for i in self.readtypes:
                i.config(state=tkinter.NORMAL)
        else:
            for i in self.readtypes:
                i.config(state=tkinter.DISABLED)
        ttk.Button(self,
                   text='Select data file(s)',
                   command=self.select_file_and_add
                   ).grid(column=0, row=4, sticky=tkinter.W, padx=3, pady=3)

    def flick_more_options(self):
        if not hasattr(self, "optframe"):
            self.optframe = ttk.Frame(self.root)
            self.optframe.grid(column=0, row=14, sticky=tkinter.W + tkinter.N,  pady=10, padx=10)
            ttk.Label(self.optframe, text="Which output format?"
                      ).grid(column=0, row=0, padx=20)
            ttk.Radiobutton(self.optframe,
                            text='png',
                            variable=self.figformat,
                            value='png',
                            ).grid(column=0, row=1, sticky=tkinter.W, padx=20, pady=3)
            for i, t in enumerate(['svg', 'pdf', 'jpeg', 'tif', 'eps']):
                ttk.Radiobutton(self.optframe,
                                text=t,
                                variable=self.figformat,
                                value=t,
                                ).grid(column=0, row=2 + i, sticky=tkinter.W, padx=20, pady=3)
            ttk.Label(self.optframe, text="Which plotting color?"
                      ).grid(column=1, row=0, padx=20)
            for i, c in enumerate(['blue', 'red', 'green', 'orange', "purple", "yellow"]):
                ttk.Radiobutton(self.optframe,
                                text=c,
                                variable=self.color,
                                value=c,
                                ).grid(column=1, row=1 + i, sticky=tkinter.W, padx=20, pady=3)
            ttk.Checkbutton(self.optframe,
                            text="Log transform read lengths",
                            variable=self.loglength
                            ).grid(column=2, row=1, sticky=tkinter.W, padx=20, pady=3)
            ttk.Checkbutton(self.optframe,
                            text="Show N50 on histograms",
                            variable=self.n50
                            ).grid(column=2, row=2, sticky=tkinter.W, padx=20, pady=3)
            vcmd = (self.register(self.validate_integer),
                    '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
            ttk.Label(self.optframe, text="Maximal read length for plotting?"
                      ).grid(column=2, row=3, padx=20)
            ttk.Entry(self.optframe,
                      textvariable=self.maxlength,
                      validate='key',
                      validatecommand=vcmd
                      ).grid(column=2, row=4, sticky=tkinter.W, padx=20, pady=3)
            ttk.Label(self.optframe, text="Minimal quality for plotting?"
                      ).grid(column=2, row=5, padx=20)
            ttk.Entry(self.optframe,
                      textvariable=self.minqual,
                      validate='key',
                      validatecommand=vcmd
                      ).grid(column=2, row=6, sticky=tkinter.W, padx=20, pady=3)
        else:
            self.optframe.grid_remove()
            delattr(self, "optframe")

    def validate_integer(self, action, index, value_if_allowed, prior_value,
                         text, validation_type, trigger_type, widget_name):
        """Check if text Entry is valid (number).

        I have no idea what all these arguments are doing here but took this from
        https://stackoverflow.com/questions/8959815/restricting-the-value-in-tkinter-entry-widget
        """
        if(action == '1'):
            if text in '0123456789.-+':
                try:
                    int(value_if_allowed)
                    return True
                except ValueError:
                    return False
            else:
                return False
        else:
            return True

    def run(self):
        self.runbutton.configure(text="Processing... please be patient.")
        self.runbutton.configure(state=tkinter.DISABLED)
        self.update_idletasks()
        try:
            utils.make_output_dir(self.destdir)
            settings = {
                'alength': False,
                "drop_outliers": False,
                "maxlength": self.maxlength.get(),
                "minqual": self.minqual.get(),
                "loglength": self.loglength.get(),
                "downsample": 10000,
                "no_N50": not self.n50.get(),
                "bam": False,
                "summary": False,
                "fastq": False,
                "readtype": self.readtype.get(),
                "barcoded": self.barcoded.get(),
                "format": check_valid_format(self.figformat.get()),
                "color": self.color.get(),
                "plots": ["kde", "hex", "dot"],
                "title": None,
                "path": self.destdir + "/",
                self.source.get(): True
            }
            datadf = get_input(
                source=self.source.get(),
                files=[self.targetfile.name],
                threads=4,
                readtype=settings["readtype"],
                combine="simple",
                barcoded=settings["barcoded"])
            write_stats(
                datadfs=[datadf],
                outputfile=settings["path"] + "NanoStats.txt")
            logging.info("Calculated statistics")
            datadf, settings = nanoplot.filter_data(datadf, settings)
            if settings["barcoded"]:
                for barc in list(datadf["barcode"].unique()):
                    settings["path"] = os.path.join(self.destdir, barc + "_")
                    dfbarc = datadf[datadf["barcode"] == barc]
                    write_stats(dfbarc, settings["path"] + "NanoStats.txt")
                    nanoplot.make_plots(dfbarc, settings)
            else:
                plots = nanoplot.make_plots(datadf, settings)
            html_report = nanoplot.make_report(
                plots=plots,
                path=settings["path"],
                logfile=self.logfile,
                statsfile=settings["path"] + "NanoStats.txt")
            logging.info("Finished!")
            self.runbutton.configure(text="Start plotting.")
            self.runbutton.configure(state=tkinter.NORMAL)
            ttk.Label(self, text="Finished, opening web browser."
                      ).grid(column=1, row=6, sticky=tkinter.W, padx=3, pady=3)
            self.update_idletasks()
            webbrowser.open(html_report, new=2)
        except Exception as e:
            logging.error(e, exc_info=True)
            ttk.Label(self, text="Something unexpected happened and NanoPlot has crashed."
                      ).grid(column=1, row=6, sticky=tkinter.W, padx=3, pady=3)
            self.update_idletasks()
            raise


import collections

from .item import ItemCollection
from .signals import navbar_created

class NavigationBar(collections.Iterable):
    """The navigation bar object."""

    def __init__(self, name, items=None, alias=None):
        self.name = name
        self.items = ItemCollection(items or [])
        self.initializers = []
        self.alias = alias or {}

        # sends signal
        navbar_created.send(self.__class__, bar=self)

    def __iter__(self):
        return iter(self.items)

    def initializer(self, fn):
        """Adds a initializer function.

        If you want to initialize the navigation bar within a Flask app
        context, you can use this decorator.

        The decorated function should nave one paramater ``nav`` which is the
        bound navigation extension instance.
        """
        self.initializers.append(fn)
        return fn

    def alias_item(self, alias):
        """Gets an item by its alias."""
        ident = self.alias[alias]
        return self.items[ident]

    @property
    def current_item(self):
        """Get the current active navigation Item if any.

        .. versionadded:: 0.2.0
        """
        return self._get_current_item(self)

    def _get_current_item(self, items):
        for item in items:
            if item.is_active:
                return item
            else:
                nested = self._get_current_item(item.items)
                if nested:
                    return nested


import operator
import collections

def freeze_dict(dict_):
    """Freezes ``dict`` into ``tuple``.

    A typical usage is packing ``dict`` into hashable.

    e.g.::

        >>> freeze_dict({'a': 1, 'b': 2})
        (('a', 1), ('b', 2))
    """
    pairs = dict_.items()
    key_getter = operator.itemgetter(0)
    return tuple(sorted(pairs, key=key_getter))

def join_html_attrs(attrs):
    """Joins the map structure into HTML attributes.

    The return value is a 2-tuple ``(template, ordered_values)``. It should be
    passed into :class:`markupsafe.Markup` to prevent XSS attacked.

    e.g.::

        >>> join_html_attrs({'href': '/', 'data-active': 'true'})
        ('data-active="{0}" href="{1}"', ['true', '/'])
    """
    attrs = collections.OrderedDict(freeze_dict(attrs or {}))
    template = ' '.join('%s="{%d}"' % (k, i) for i, k in enumerate(attrs))
    return template, list(attrs.values())

class BoundTypeProperty(object):
    """This kind of property creates subclasses of given class for each
    instance.

    Those subclasses means "bound type" which be used for identifying
    themselves with blinker/Flask signals.

    e.g.::

        >>> class Foo(object):
        ...     pass
        >>> class Bar(object):
        ...     Foo = BoundTypeProperty('Foo', Foo)
        >>>
        >>> Bar.Foo
        BoundTypeProperty('Foo', Foo)
        >>> bar = Bar()
        >>> bar.Foo is Foo
        False
        >>> issubclass(bar.Foo, Foo)
        True
        >>> egg = Bar()
        >>> egg.Foo is bar.Foo
        False
        >>> egg.Foo.__bases__ == bar.Foo.__bases__ == (Foo,)
        True

    :param name: the name of this property.
    :param cls: the base class of all bound classes.
    """

    def __init__(self, name, cls):
        self.name = name
        self.cls = cls

    def __repr__(self):
        return 'BoundTypeProperty(%r, %s)' % (self.name, self.cls.__name__)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        ns = vars(instance)  # the instance namespace
        if self.name not in ns:
            ns[self.name] = type(self.name, (self.cls,), {})
        return ns[self.name]


from flask.signals import appcontext_pushed

from .navbar import NavigationBar
from .item import Item, ItemReference
from .utils import BoundTypeProperty
from .signals import navbar_created

class Navigation(object):
    """The navigation extension API."""

    #: The subclass of :class:`~flask.ext.navigation.navbar.NavigationBar`. It
    #: bound with the the current instance.
    #:
    #: :type: :class:`~flask.ext.navigation.utils.BoundTypeProperty`
    Bar = BoundTypeProperty('Bar', NavigationBar)

    #: The subclass of :class:`~flask.ext.navigation.item.Item`. It bound with
    #: the the current instance.
    #:
    #: :type: :class:`~flask.ext.navigation.utils.BoundTypeProperty`
    Item = BoundTypeProperty('Item', Item)

    ItemReference = ItemReference

    def __init__(self, app=None):
        self.bars = {}
        if app is not None:
            self.init_app(app)
        # connects ext-level signals
        navbar_created.connect(self.bind_bar, self.Bar)

    def __getitem__(self, name):
        """Gets a bound navigation bar by its name."""
        return self.bars[name]

    def init_app(self, app):
        """Initializes an app to work with this extension.

        The app-context signals will be subscribed and the template context
        will be initialized.

        :param app: the :class:`flask.Flask` app instance.
        """
        # connects app-level signals
        appcontext_pushed.connect(self.initialize_bars, app)
        # integrate with jinja template
        app.add_template_global(self, 'nav')

    def initialize_bars(self, sender=None, **kwargs):
        """Calls the initializers of all bound navigation bars."""
        for bar in self.bars.values():
            for initializer in bar.initializers:
                initializer(self)

    def bind_bar(self, sender=None, **kwargs):
        """Binds a navigation bar into this extension instance."""
        bar = kwargs.pop('bar')
        self.bars[bar.name] = bar


import collections

from flask import url_for, request, Markup

from .utils import freeze_dict, join_html_attrs

class Item(object):
    """The navigation item object.

    :param label: the display label of this navigation item.
    :param endpoint: the unique name of this navigation item.
                     If this item point to a internal url, this parameter
                     should be acceptable for ``url_for`` which will generate
                     the target url.
    :param args: optional. If this parameter be provided, it will be passed to
                 the ``url_for`` with ``endpoint`` together.
                 Maybe this arguments need to be decided in the Flask app
                 context, then this parameter could be a function to delay the
                 execution.
    :param url: optional. If this parameter be provided, the target url of
                this navigation will be it. The ``endpoint`` and ``args`` will
                not been used to generate url.
    :param html_attrs: optional. This :class:`dict` will be used for
                       representing html.

    The ``endpoint`` is the identity name of this navigation item. It will be
    unique in whole application. In mostly situation, it should be a endpoint
    name of a Flask view function.
    """

    def __init__(self, label, endpoint, args=None, url=None, html_attrs=None,
                 items=None):
        self.label = label
        self.endpoint = endpoint
        self._args = args
        self._url = url
        self.html_attrs = {} if html_attrs is None else html_attrs
        self.items = ItemCollection(items or None)

    def __html__(self):
        attrs = dict(self.html_attrs)

        # adds ``active`` to class list
        html_class = attrs.get('class', [])
        if self.is_active:
            html_class.append('active')

        # joins class list
        attrs['class'] = ' '.join(html_class)
        if not attrs['class']:
            del attrs['class']
        attrs['href'] = self.url
        attrs_template, attrs_values = join_html_attrs(attrs)

        return Markup('{label}' % attrs_template).format(
            *attrs_values, label=self.label)

    def __html_format__(self, format_spec):
        if format_spec == 'li':
            li_attrs = Markup(' class="active"') if self.is_active else ''
            return Markup('
{1}
').format(li_attrs, self.__html__())
        elif format_spec:
            raise ValueError('Invalid format spec')
        return self.__html__()

    @property
    def args(self):
        """The arguments which will be passed to ``url_for``.

        :type: :class:`dict`
        """
        if self._args is None:
            return {}
        if callable(self._args):
            return dict(self._args())
        return dict(self._args)

    @property
    def url(self):
        """The final url of this navigation item.

        By default, the value is generated by the :attr:`self.endpoint` and
        :attr:`self.args`.

        .. note::

           The :attr:`url` property require the app context without a provided
           config value :const:`SERVER_NAME`, because of :func:`flask.url_for`.

        :type: :class:`str`
        """
        if self.is_internal:
            return url_for(self.endpoint, **self.args)
        return self._url

    @property
    def is_active(self):
        """``True`` if the item should be presented as active, and ``False``
        always if the request context is not bound.
        """
        return bool(request and self.is_current)

    @property
    def is_internal(self):
        """``True`` if the target url is internal of current app."""
        return self._url is None

    @property
    def is_current(self):
        """``True`` if current request has same endpoint with the item.

        The property should be used in a bound request context, or the
        :class:`RuntimeError` may be raised.
        """
        if not self.is_internal:
            return False  # always false for external url
        has_same_endpoint = (request.endpoint == self.endpoint)
        has_same_args = (request.view_args == self.args)
        return has_same_endpoint and has_same_args  # matches the endpoint

    @property
    def ident(self):
        """The identity of this item.

        :type: :class:`~flask.ext.navigation.Navigation.ItemReference`
        """
        return ItemReference(self.endpoint, self.args)

class ItemCollection(collections.MutableSequence,
                     collections.Iterable):
    """The collection of navigation items.

    This collection is a mutable sequence. All items have order index, and
    could be found by its endpoint name. e.g.::

        c = ItemCollection()
        c.append(Item(endpoint='doge'))

        print(c['doge'])  # output: Item(endpoint='doge')
        print(c[0])       # output: Item(endpoint='doge')
        print(c)          # output: ItemCollection([Item(endpoint='doge')])
        print(len(c))     # output: 1

        c.append(Item(endpoint='lumpy', args={'num': 4}))

        print(c[1])       # output: Item(endpoint='lumpy', args={'num': 4})
        assert c['lumpy', {'num': 4}] is c[1]
    """

    def __init__(self, iterable=None):
        #: the item collection
        self._items = []
        #: the mapping collection of endpoint -> item
        self._items_mapping = {}
        #: initial extending
        self.extend(iterable or [])

    def __repr__(self):
        return 'ItemCollection(%r)' % self._items

    def __getitem__(self, index):
        if isinstance(index, int):
            return self._items[index]

        if isinstance(index, tuple):
            endpoint, args = index
        else:
            endpoint, args = index, {}
        ident = ItemReference(endpoint, args)
        return self._items_mapping[ident]

    def __setitem__(self, index, item):
        # remove the old reference
        old_item = self._items[index]
        del self._items_mapping[old_item.ident]

        self._items[index] = item
        self._items_mapping[item.ident] = item

    def __delitem__(self, index):
        item = self[index]
        del self._items[index]
        del self._items_mapping[item.ident]

    def __len__(self):
        return len(self._items)

    def __iter__(self):
        return iter(self._items)

    def insert(self, index, item):
        self._items.insert(index, item)
        self._items_mapping[item.ident] = item

class ItemReference(collections.namedtuple('ItemReference', 'endpoint args')):
    """The identity tuple of navigation item.

    :param endpoint: the endpoint of view function.
    :type endpoint: ``str``
    :param args: the arguments of view function.
    :type args: ``dict``
    """

    def __new__(cls, endpoint, args=()):
        if isinstance(args, dict):
            args = freeze_dict(args)
        return super(cls, ItemReference).__new__(cls, endpoint, args)


from __future__ import print_function

import collections
import os
import logging
import re
import requests

from .errors import BadArgumentError

class Speller(object):
    """
    Base spell class. Implements spelling logic for files.
    """

    def spell(self, text):
        if isinstance(text, (list, tuple)):
            text = ','.join(text)

        text = self._prepare_text(text)

        if text.startswith(('http://', 'https://')):
            yield self._spell_url(text)

        elif os.path.exists(text):
            for item in self._spell_path(text):
                yield item

        elif text:
            for item in self._spell_text(text):
                yield item

        else:
            raise NotImplementedError()

    def _spell_text(self, text):
        logging.info("spelling text: " + text)
        return NotImplemented

    def _spell_url(self, url):
        logging.info("spelling url: " + url)
        content = requests.get(url)
        for item in self._spell_text(content):
            yield item

    def _spell_file(self, path):
        with open(path) as f:
            content = f.read()
            for item in self._spell_text(content):
                yield item

    def _spell_path(self, path):
        logging.info("spelling path: " + path)
        if os.path.isfile(path):
            yield self._spell_file(path)
        else:
            for root, dirs, fnames in os.walk(path):
                for fname in fnames:
                    fullpath = os.path.join(root, fname)
                    print(fullpath, end=': ')
                    for res in self._spell_file(fullpath):
                        yield res

    def _prepare_text(self, text):
        subs = {
            '\r\n': '\n',  # Fix Windows
            '\r': '\n',  # Fix MacOS
            '\s+\n': '\n',  # Trailing spaces
            '\s+': ' ',  # Repeat spaces
            '\n+': '\n',  # Repeat line ends
        }
        for src, dst in subs.items():
            text = text.replace(src, dst)
        return text.strip()

    def _strip_tags(self, text):
        return re.sub('<[^<]+?>', '', text)

class YandexSpeller(Speller):
    """
    Yandex speller implementation.
    """
    _supported_langs = {'en', 'ru', 'uk'}

    def __init__(self, format_text=None, lang=None, config_path=None,
                 dictionary=None, report_type=None, max_requests=2,
                 is_debug=False, check_yo=False, ignore_urls=False,
                 ignore_tags=False, ignore_capitalization=False,
                 ignore_digits=False, ignore_latin=False,
                 ignore_roman_numerals=False, ignore_uppercase=False,
                 find_repeat_words=False, flag_latin=False, by_words=False):

        self._lang = ['en', 'ru']
        self.lang = lang
        self._format = format_text
        self._config_path = config_path or ''
        self._dictionary = dictionary or {}
        self._report_type = report_type or 'console'

        self._check_yo = check_yo
        self._ignore_urls = ignore_urls
        self._ignore_tags = ignore_tags
        self._ignore_capitalization = ignore_capitalization
        self._ignore_digits = ignore_digits
        self._ignore_latin = ignore_latin
        self._ignore_roman_numerals = ignore_roman_numerals
        self._ignore_uppercase = ignore_uppercase
        self._find_repeat_words = find_repeat_words
        self._flag_latin = flag_latin
        self._by_words = by_words

        self._max_requests = max_requests
        self._is_debug = is_debug

        self._api_query = 'http://speller.yandex.net/services/' \
                          'spellservice.json/checkText'

    @property
    def format(self):
        """Get format"""
        return self._format

    @format.setter
    def format(self, value):
        """Set format"""
        self._format = value

    @property
    def lang(self):
        """Get lang"""
        return self._lang

    @lang.setter
    def lang(self, language):
        """Set lang"""
        if isinstance(language, str):
            self._lang = [language]
        elif isinstance(language, collections.Iterable):
            self._lang = list(language)

        if any(lang not in self._supported_langs for lang in self._lang):
            raise BadArgumentError("Unsupported language")

    @property
    def config_path(self):
        """Get config_path"""
        return self._config_path

    @config_path.setter
    def config_path(self, value):
        """Set config_path"""
        self._config_path = value or ''
        if not isinstance(self._config_path, str):
            raise BadArgumentError("config_path must be string: {}".format(
                self._config_path))

    @property
    def dictionary(self):
        """Get dictionary"""
        return self._dictionary

    @dictionary.setter
    def dictionary(self, value):
        """Set dictionary"""
        self._dictionary = value or {}
        if not isinstance(self._dictionary, dict):
            raise BadArgumentError("dictionary must be dict: {}".format(
                self._dictionary))

    @property
    def report_type(self):
        """Get report_type"""
        return self._report_type

    @report_type.setter
    def report_type(self, value):
        """Set report_type"""
        self._report_type = value or 'console'

    @property
    def check_yo(self):
        """Get check_yo"""
        return self._check_yo

    @check_yo.setter
    def check_yo(self, value):
        """Set check_yo"""
        self._check_yo = value

    @property
    def ignore_urls(self):
        """Get ignore_urls"""
        return self._ignore_urls

    @ignore_urls.setter
    def ignore_urls(self, value):
        """Set ignore_urls"""
        self._ignore_urls = value

    @property
    def ignore_tags(self):
        """Get ignore_tags"""
        return self._ignore_tags

    @ignore_tags.setter
    def ignore_tags(self, value):
        """Set ignore_tags"""
        self._ignore_tags = value

    @property
    def ignore_capitalization(self):
        """Get ignore_capitalization"""
        return self._ignore_capitalization

    @ignore_capitalization.setter
    def ignore_capitalization(self, value):
        """Set ignore_capitalization"""
        self._ignore_capitalization = value

    @property
    def ignore_digits(self):
        """Get ignore_digits"""
        return self._ignore_digits

    @ignore_digits.setter
    def ignore_digits(self, value):
        """Set ignore_digits"""
        self._ignore_digits = value

    @property
    def ignore_latin(self):
        """Get ignore_latin"""
        return self._ignore_latin

    @ignore_latin.setter
    def ignore_latin(self, value):
        """Set ignore_latin"""
        self._ignore_latin = value

    @property
    def ignore_roman_numerals(self):
        """Get ignore_roman_numerals"""
        return self._ignore_roman_numerals

    @ignore_roman_numerals.setter
    def ignore_roman_numerals(self, value):
        """Set ignore_roman_numerals"""
        self._ignore_roman_numerals = value

    @property
    def ignore_uppercase(self):
        """Get ignore_uppercase"""
        return self._ignore_uppercase

    @ignore_uppercase.setter
    def ignore_uppercase(self, value):
        """Set ignore_uppercase"""
        self._ignore_uppercase = value

    @property
    def find_repeat_words(self):
        """Get find_repeat_words"""
        return self._find_repeat_words

    @find_repeat_words.setter
    def find_repeat_words(self, value):
        """Set find_repeat_words"""
        self._find_repeat_words = value

    @property
    def flag_latin(self):
        """Get flag_latin"""
        return self._flag_latin

    @flag_latin.setter
    def flag_latin(self, value):
        """Set flag_latin"""
        self._flag_latin = value

    @property
    def by_words(self):
        """Get by_words"""
        return self._by_words

    @by_words.setter
    def by_words(self, value):
        """Set by_words"""
        self._by_words = value

    @property
    def max_requests(self):
        """Get max_requests"""
        return self._max_requests

    @max_requests.setter
    def max_requests(self, value):
        """Set max_requests"""
        self._max_requests = value

    @property
    def is_debug(self):
        """Get is_debug"""
        return self._is_debug

    @is_debug.setter
    def is_debug(self, value):
        """Set is_debug"""
        self._is_debug = value

    def _spell_text(self, text):
        lang = ','.join(self._lang)
        data = {
            'text': text,
            'options': self.api_options,
            'lang': lang,
        }
        if self.format:
            data['format'] = self.format
        response = requests.post(url=self._api_query, data=data).json()
        logging.debug('{}?{}'.format(
            self._api_query, requests.compat.urlencode(data)))
        logging.debug("response: " + str(response))
        return response

    @property
    def api_options(self):
        """
        current spelling settings
        :return: api options as number
        """
        options = 0
        if self._ignore_uppercase:
            options |= 1
        if self._ignore_digits:
            options |= 2
        if self._ignore_urls:
            options |= 4
        if self._find_repeat_words:
            options |= 8
        if self._ignore_latin:
            options |= 16
        if self._flag_latin:
            options |= 128
        if self._by_words:
            options |= 256
        if self._ignore_capitalization:
            options |= 512
        if self._ignore_roman_numerals:
            options |= 2048
        return options

class Word(object):
    """
    Class for quick spelling of single word.
    """

    def __init__(self, *args, **kwargs):
        if 'text' in kwargs:
            text = kwargs.pop('text')
        else:
            text = args[0]
        super(Word, self).__init__(*args[1:], **kwargs)

        if len(text.split()) > 1:
            raise BadArgumentError('Bad argument. Several words detected.')

        self._spell_text = YandexSpeller(*args[1:], **kwargs)._spell_text
        self.text = text
        self._answer = None

    @property
    def answer(self):
        if self._answer is None:
            self._answer = self._spell_text(self.text)
        return self._answer

    @property
    def correct(self):
        return not self.answer

    @property
    def variants(self):
        if self.correct:
            return
        return self.answer[0]['s']

    @property
    def spellsafe(self):
        if self.correct:
            return
        try:
            return self.variants[0]
        except IndexError:
            return


from django.core.exceptions import ImproperlyConfigured

def validate(metric_class):
    """
    Does basic Metric option validation.
    """
    if not hasattr(metric_class, 'label'):
        raise ImproperlyConfigured("No 'label' attribute found for metric %s." % metric_class.__name__)

    if not hasattr(metric_class, 'widget'):
        raise ImproperlyConfigured("No 'widget' attribute found for metric %s." % metric_class.__name__)


from django.utils.translation import ugettext as _

from analytics import settings
from analytics.options import Statistic

def list_statistics():
    """
    Prints all of the available statistics.
    """
    for s in get_statistic_models():
        print s.__name__, "(%s)" % s

def ensure_list(v):
    """
    Makes sure that the given value is a list.
    """
    return list(v) if getattr(v, '__iter__', False) else [v]

def get_statistic_by_name(stat_name):
    """
    Fetches a statistics based on the given class name. Does a look-up
    in the gadgets' registered statistics to find the specified one.
    """

    if stat_name == 'ALL':
        return get_statistic_models()

    for stat in get_statistic_models():
        if stat.__name__ == stat_name:
            return stat

    raise Exception, _("%(stat)s cannot be found.") % {'stat': stat_name}

def get_statistic_models():
    from django.db.models import get_models
    import inspect

    stats = []
    for model in get_models():
        if Statistic in inspect.getmro(model):
            stats.append(model)

    return stats

def calculate_statistics(stat, frequencies):
    """
    Calculates all of the metrics associated with the registered gadgets.
    """
    stats = ensure_list(stat)
    frequencies = ensure_list(frequencies)

    for stat in stats:
        for f in frequencies:
            print "Calculating %s (%s)..." % (stat.__name__, settings.STATISTIC_FREQUENCY_DICT[f])
            stat.calculate(f)

def reset_statistics(stat, frequencies, reset_cumulative, recalculate=False):
    """
    Resets the specified statistic's data (deletes it) for the given
    frequency/ies.
    """

    stats = ensure_list(stat)
    frequencies = ensure_list(frequencies)

    for s in stats:
        for f in frequencies:
            if not s.cumulative or reset_cumulative:
                print "Resetting %s (%s)..." % (s.__name__, settings.STATISTIC_FREQUENCY_DICT[f])
                s.objects.filter(frequency=f).delete()
            elif s.cumulative and not reset_cumulative:
                print "Skipping %s because it is cumulative." % s.__name__

    if recalculate:
        print "Recalculating statistics..."
        calculate_statistics(stats, frequencies)


def autodiscover():
    """
    Auto-discover INSTALLED_APPS gadgets.py modules and fail silently when
    not present. This forces an import on them to register any gadgets they
    may want.
    """
    from django.conf import settings
    from django.utils.importlib import import_module
    from django.utils.module_loading import module_has_submodule

    for app in settings.INSTALLED_APPS:
        mod = import_module(app)
        # Attempt to import the app's gadgets module.
        try:
            import_module('%s.gadgets' % app)
        except:
            # Decide whether to bubble up this error. If the app just
            # doesn't have a gadgets module, we can ignore the error
            # attempting to import it, otherwise we want it to bubble up.
            if module_has_submodule(mod, 'gadgets'):
                raise

autodiscover()


import csv
import datetime
from django.utils.translation import ugettext as _
from django.http import HttpResponse
from analytics import settings

def csv_dump(request, uid):
    """
    Returns a CSV dump of all of the specified metric's counts
    and cumulative counts.
    """

    metric = Metric.objects.get(uid=uid)
    frequency = request.GET.get('frequency', settings.STATISTIC_FREQUENCY_DAILY)

    response = HttpResponse(mimetype='text/csv')
    response['Content-Disposition'] = 'attachment; filename=%s%s.csv' % (uid, datetime.datetime.now().strftime("%Y%m%d-%H%M"))

    writer = csv.writer(response)
    writer.writerow([_('Date/time'), _('Count'), _('Cumulative count')])
    for stat in metric.statistics.filter(frequency=frequency).order_by('date_time'):
        writer.writerow([stat.date_time.strftime(settings.CSV_DATETIME_FORMAT), stat.count, stat.cumulative_count])

    return response


from datetime import datetime, timedelta
import re

from django.db import models
from django.utils.translation import ugettext as _

from analytics import settings

class Statistic(models.Model):
    """
    A counter (point and cumulative) for a specific metric relevant
    to a particular date/time.

    A 'label' member is required to be provided by inheriting classes, to be used as user friendly metric title during widget rendering.
    A 'widget' member is required to be provided by inheriting classes, to be used as for rendering the metric.
    """

    cumulative = False

    class Meta:
        abstract = True

    date_time = models.DateTimeField(
        db_index=True,
    )
    frequency = models.CharField(
        max_length=1,
        choices=settings.STATISTIC_FREQUENCY_CHOICES,
        db_index=True,
    )
    count = models.IntegerField(
        default=0,
    )
    cumulative_count = models.BigIntegerField(
        default=0,
    )

    def __unicode__(self):
        return u'%s' % self.__class__.get_label()

    @classmethod
    def get_label(cls):
        """
        Returns a label for this class.
        """
        return ' '.join(re.findall(r'[A-Z][^A-Z]*', cls.__name__)).capitalize()

    @classmethod
    def calculate(cls, frequency=settings.STATISTIC_FREQUENCY_DAILY, verbose=settings.STATISTIC_CALCULATION_VERBOSE):
        """
        Runs the calculator for this type of statistic.
        """
        if verbose:
            print _("Calculating statistics for %(class)s...") % {'class': cls.get_label()}

        start_datetime = None
        end_datetime = None

        # get the latest statistic
        latest_stat = cls.get_latest(frequency)

        # work out today's date, truncated to midnight
        today = datetime.strptime(datetime.now().strftime("%Y %m %d"), "%Y %m %d")
        now = datetime.now()

        # if this statistic only has cumulative stats available
        if cls.cumulative:
            if frequency == settings.STATISTIC_FREQUENCY_HOURLY:
                # truncate to the nearest hour
                start_datetime = datetime.strptime(now.strftime("%Y %m %d %H:00:00"), "%Y %m %d %H:%M:%S")
            elif frequency == settings.STATISTIC_FREQUENCY_DAILY:
                start_datetime = today
            elif frequency == settings.STATISTIC_FREQUENCY_WEEKLY:
                # truncate today to the start of this week
                start_datetime = datetime.strptime(today.strftime("%Y %W 0"), "%Y %W %w")
            elif frequency == settings.STATISTIC_FREQUENCY_MONTHLY:
                # truncate today to the start of this month
                start_datetime = datetime.strptime(today.strftime("%Y %m 1"), "%Y %m %d")

            stat, created = cls.objects.get_or_create(date_time=start_datetime, frequency=frequency)
            stat.cumulative_count = cls.get_cumulative()
            stat.count = (stat.cumulative_count-latest_stat.cumulative_count) if latest_stat else stat.cumulative_count
        else:
            # get the date/time at which we should start calculating
            start_datetime = cls.get_start_datetime() if latest_stat is None else latest_stat.date_time
            # truncate the start date/time to the appropriate frequency
            if frequency == settings.STATISTIC_FREQUENCY_HOURLY:
                start_datetime = datetime.strptime(start_datetime.strftime("%Y %m %d %H:00:00"), "%Y %m %d %H:%M:%S")
                end_datetime = start_datetime+timedelta(hours=1)
            elif frequency == settings.STATISTIC_FREQUENCY_DAILY:
                start_datetime = datetime.strptime(start_datetime.strftime("%Y %m %d"), "%Y %m %d")
                end_datetime = start_datetime+timedelta(days=1)
            elif frequency == settings.STATISTIC_FREQUENCY_WEEKLY:
                # start at the beginning of the week of the latest stat
                start_datetime = datetime.strptime(start_datetime.strftime("%Y %W 0"), "%Y %W %w")-timedelta(days=7)
                end_datetime = start_datetime+timedelta(days=7)
            elif frequency == settings.STATISTIC_FREQUENCY_MONTHLY:
                # start at the beginning of the month of the latest stat
                start_datetime = datetime.strptime(start_datetime.strftime("%Y %m 1"), "%Y %m %d")
                end_datetime = datetime.strptime((start_datetime+timedelta(days=33)).strftime("%Y %m 1"), "%Y %m %d")

            # if we're doing the normal count
            while start_datetime < now:
                count = cls.get_count(start_datetime, end_datetime)
                cumulative_count = 0
                if isinstance(count, tuple):
                    cumulative_count = count[1]
                    count = count[0]
                else:
                    cumulative_count = (latest_stat.cumulative_count+count) if latest_stat else count

                stat, created = cls.objects.get_or_create(date_time=start_datetime, frequency=frequency)
                stat.count = count
                stat.cumulative_count = cumulative_count
                stat.save()

                latest_stat = stat

                # update the dates/time window
                start_datetime = end_datetime
                if frequency == settings.STATISTIC_FREQUENCY_HOURLY:
                    end_datetime += timedelta(hours=1)
                elif frequency == settings.STATISTIC_FREQUENCY_DAILY:
                    end_datetime += timedelta(days=1)
                elif frequency == settings.STATISTIC_FREQUENCY_WEEKLY:
                    end_datetime += timedelta(days=7)
                elif frequency == settings.STATISTIC_FREQUENCY_MONTHLY:
                    end_datetime = datetime.strptime((start_datetime+timedelta(days=33)).strftime("%Y %m 1"), "%Y %m %d")

    @classmethod
    def get_count(cls, start_datetime, end_datetime):
        """
        Must calculate the number of statistics between the two
        specified date/times. These date/times are passed from the
        calculator functions depending on the type of calculation
        being performed.

        Results must be returned for date >= start_datetime and
        date < end_datetime.
        """
        raise NotImplementedError("%s has to implement 'get_count' method" % cls.__name__)

    @classmethod
    def get_cumulative(cls):
        """
        Must return cumulative count for statistic.
        """
        raise NotImplementedError("%s has to implement 'get_cumulative' method" % cls.__name__)

    @classmethod
    def get_start_datetime(cls):
        """
        Must return a date/time object indicating when the earliest
        data available for this metric occurred.
        """
        return settings.STATISTIC_DEFAULT_START_DATETIME

    @classmethod
    def get_latest(cls, frequency=settings.STATISTIC_FREQUENCY_DAILY):
        """
        Returns the latest instance of this statistic.
        """
        try:
            return cls.objects.filter(frequency=frequency).order_by('-date_time')[0]
        except IndexError:
            return None

    @classmethod
    def get_earliest(cls, frequency=settings.STATISTIC_FREQUENCY_DAILY):
        """
        Returns the earliest instance of this statistic.
        """
        try:
            return cls.objects.filter(frequency=frequency).order_by('date_time')[0]
        except IndexError:
            return None

    @classmethod
    def latest_count(cls, frequency=settings.STATISTIC_FREQUENCY_DAILY, cumulative=True, count=False):
        """
        Returns the latest count for the given frequency.
        """
        latest_stat = cls.latest(frequency=frequency)
        if latest_stat:
            if count and cumulative:
                return (latest_stat.count, latest_stat.cumulative_count)
            elif count:
                return latest_stat.count
            else:
                return latest_stat.cumulative_count
        else:
            if count and cumulative:
                return (0, 0)
            else:
                return 0


#
# Central management command for django-analytics.
#

from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from django.utils.translation import ugettext as _
from analytics import maintenance
from analytics import settings

class Command(BaseCommand):
    """
    The management command to handle statistic-related function calls.
    """

    option_list = BaseCommand.option_list + (
        make_option('-l', '--list',
            action='store_true',
            dest='list',
            default=False,
            help=_("List all of the available statistics."),
        ),
        make_option('-c', '--calculate',
            action='store',
            type='string',
            dest='calculate',
            default=None,
            help=_("Perform the calculation for the specified statistic. Use --calculate=ALL to calculate all statistics."),
        ),
        make_option('--reset',
            action='store',
            dest='reset',
            type='string',
            default=None,
            help=_("Reset the specified statistic. Use --reset=ALL to reset all statistics."),
        ),
        make_option('--recalculate',
            action='store',
            dest='recalculate',
            type='string',
            default=None,
            help=_("Recalculates the specified statistic. Use --recalculate=ALL to recalculate all statistics."),
        ),
        make_option('--reset-cumulative',
            action='store_true',
            dest='reset_cumulative',
            default=False,
            help=_("Should the cumulative statistics also be reset (if --reset is used)? Default: False"),
        ),
        make_option('-f', '--frequency',
            action='store',
            dest='frequency',
            type='string',
            default='a',
            help=_("Set the frequency for the current calculation. h=hourly, d=daily, w=weekly, m=monthly, a=all. Separate multiple frequencies by commas, e.g. d,w,m"),
        ),
    )

    help = "Analytics-related functionality"

    def handle(self, *args, **kwargs):
        """
        Command handler for the "metrics" command.
        """

        frequency = kwargs['frequency']
        frequencies = settings.STATISTIC_FREQUENCY_ALL if frequency == 'a' else (frequency.split(',') if ',' in frequency else [frequency])

        if kwargs['list']:
            maintenance.list_statistics()

        # if we're supposed to calculate the latest statistics
        elif kwargs['calculate']:
            maintenance.calculate_statistics(maintenance.get_statistic_by_name(kwargs['calculate']), frequencies)

        # pure reset of statistic(s)
        elif kwargs['reset']:
            maintenance.reset_statistics(maintenance.get_statistic_by_name(kwargs['reset']), frequencies, kwargs['reset_cumulative'])

        # recalculation of statistic(s)
        elif kwargs['recalculate']:
            maintenance.reset_statistics(maintenance.get_statistic_by_name(kwargs['recalculate']), frequencies, kwargs['reset_cumulative'], True)


#
# Geckoboard-related views for django-analytics.
#
# thane@praekelt.com
#

from datetime import datetime, timedelta
from django_geckoboard.decorators import number_widget, rag_widget, pie_chart, line_chart, geck_o_meter, funnel
from analytics import settings
from django.utils.translation import ugettext as _

def get_GET_array(request, var_name, fail_silently=True):
    """
    Returns the GET array's contents for the specified variable.
    """

    vals = request.GET.getlist(var_name)
    if not vals:
        if fail_silently:
            return []
        else:
            raise Exception, _("No array called '%(varname)s' in GET variables") % {'varname': var_name}

    return vals

def get_GET_bool(request, var_name, default=True):
    """
    Tries to extract a boolean variable from the specified request.
    """

    val = request.GET.get(var_name, default)
    if isinstance(val, str) or isinstance(val, unicode):
        val = True if val[0] == 't' else False

    return val

def get_next_colour():
    """
    Gets the next colour in the Geckoboard colour list.
    """

    colour = settings.GECKOBOARD_COLOURS[get_next_colour.cur_colour]

    get_next_colour.cur_colour += 1
    if get_next_colour.cur_colour >= len(settings.GECKOBOARD_COLOURS):
        get_next_colour.cur_colour = 0

    return colour

get_next_colour.cur_colour = 0

def get_gecko_params(request, uid=None, days_back=0, cumulative=True,
    frequency=settings.STATISTIC_FREQUENCY_DAILY, min_val=0, max_val=100,
    chart_type='standard', percentage='show', sort=False):
    """
    Returns the default GET parameters for a particular Geckoboard
    view request.
    """

    return {
        'days_back'  : int(request.GET.get('daysback', days_back)),
        'uid'        : request.GET.get('uid', uid),
        'uids'       : get_GET_array(request, 'uids[]'),
        'cumulative' : get_GET_bool(request, 'cumulative', cumulative),
        'frequency'  : request.GET.get('frequency', frequency),
        'min'        : request.GET.get('min', min_val),
        'max'        : request.GET.get('max', max_val),
        'type'       : request.GET.get('type', chart_type),
        'percentage' : request.GET.get('percentage', percentage),
        'sort'       : get_GET_bool(request, 'sort', sort),
    }

@number_widget
def geckoboard_number_widget(request):
    """
    Returns a number widget for the specified metric's cumulative total.
    """

    params = get_gecko_params(request, days_back=7)
    metric = Metric.objects.get(uid=params['uid'])
    try:
        latest_stat = metric.statistics.filter(frequency=params['frequency']).order_by('-date_time')[0]
    except IndexError:
        return (0, 0)

    try:
        prev_stat = metric.statistics.filter(frequency=params['frequency'],
            date_time__lte=latest_stat.date_time-timedelta(days=params['days_back'])).order_by('-date_time')[0]
    except IndexError:
        # if there is no previous stat
        return (latest_stat.cumulative_count, 0) if params['cumulative'] else (latest_stat.count, 0)

    return (latest_stat.cumulative_count, prev_stat.cumulative_count) if params['cumulative'] else (latest_stat.count, prev_stat.count)

@rag_widget
def geckoboard_rag_widget(request):
    """
    Searches the GET variables for metric UIDs, and displays
    them in a RAG widget.
    """

    params = get_gecko_params(request)
    print params['uids']
    max_date = datetime.now()-timedelta(days=params['days_back'])

    metrics = Metric.objects.filter(uid__in=params['uids'])
    results = [(metric.latest_count(frequency=params['frequency'], count=not params['cumulative'],
        cumulative=params['cumulative'], max_date=max_date), metric.title) for metric in metrics]

    return tuple(results)

@pie_chart
def geckoboard_pie_chart(request):
    """
    Shows a pie chart of the metrics in the uids[] GET variable array.
    """

    params = get_gecko_params(request, cumulative=True)
    from_date = datetime.now()-timedelta(days=params['days_back'])

    metrics = Metric.objects.filter(uid__in=params['uids'])
    results = [(metric.latest_count(frequency=params['frequency'], count=not params['cumulative'],
        cumulative=params['cumulative']), metric.title, get_next_colour()) for metric in metrics]

    return tuple(results)

@line_chart
def geckoboard_line_chart(request):
    """
    Returns the data for a line chart for the specified metric.
    """

    params = get_gecko_params(request, cumulative=False, days_back=7)
    metric = Metric.objects.get(uid=params['uid'])

    start_date = datetime.now()-timedelta(days=params['days_back'])
    stats = [s for s in metric.statistics.filter(frequency=params['frequency'],
        date_time__gte=start_date).order_by('date_time')]

    if len(stats) == 0:
        raise Exception, _("No statistics for metric %(metric)s.") % {'metric': params['uid']}

    dates = [stats[0].date_time]

    # get up to 3 dates from the stats
    if len(stats) >= 3:
        mid = len(stats)/2
        if not mid:
            mid = 1
        dates.extend([stats[mid].date_time, stats[-1].date_time])
    elif len(stats) == 2:
        dates.extend([stats[-1].date_time])

    return (
        [s.count for s in stats],
        dates,
        metric.title,
    )

@geck_o_meter
def geckoboard_geckometer(request):
    """
    Returns a Geck-o-Meter control for the specified metric.
    """

    params = get_gecko_params(request, cumulative=True)
    metric = Metric.objects.get(uid=params['uid'])

    return (metric.latest_count(frequency=params['frequency'], count=not params['cumulative'],
        cumulative=params['cumulative']), params['min'], params['max'])

@funnel
def geckoboard_funnel(request, frequency=settings.STATISTIC_FREQUENCY_DAILY):
    """
    Returns a funnel chart for the metrics specified in the GET variables.
    """

    # get all the parameters for this function
    params = get_gecko_params(request, cumulative=True)
    metrics = Metric.objects.filter(uid__in=params['uids'])
    items = [(metric.latest_count(frequency=params['frequency'], count=not params['cumulative'],
        cumulative=params['cumulative']), metric.title) for metric in metrics]

    return {
        'items'     : items,
        'type'      : params['type'],
        'percentage': params['percentage'],
        'sort'      : params['sort'],
    }


from django.views.generic import TemplateView

from analytics import maintenance

class AlreadyRegistered(Exception):
    pass

class AnalyticsView(TemplateView):
    columns = 4
    rows = 2

    def get_template_names(self):
        return 'analytics/dashboard.html'

    def __init__(self):
        self._registry = []

    def get_gadgets(self):
        return self._registry

    def get_active_stats(self):
        """
        Returns all of the active statistics for the gadgets currently registered.
        """
        stats = []
        for gadget in self._registry.values():
            for s in gadget.stats:
                if s not in stats:
                    stats.append(s)
        return stats

    def register(self, gadget):
        """
        Registers a gadget object.
        If a gadget is already registered, this will raise AlreadyRegistered.
        """
        if gadget in self._registry:
            raise AlreadyRegistered
        else:
            self._registry.append(gadget)

    def unregister(self, gadgets):
        """
        Unregisters the specified gadget(s) if it/they has/have already been registered.
        "gadgets" can be a single class or a tuple/list of classes to unregister.
        """
        gadgets = maintenance.ensure_list(gadgets)
        for gadget in gadgets:
            while gadget in self._registry:
                self._registry.remove(gadget)

    def get_max_dimension(self):
        columns = 0
        rows = 0
        for gadget in self._registry:
            if gadget.columns > columns:
                columns = gadget.columns
            if gadget.rows > rows:
                rows = gadget.rows

        return columns, rows

    def get_context_data(self, **kwargs):
        """
        Get the context for this view.
        """
        #max_columns, max_rows = self.get_max_dimension()
        context = {
            'gadgets': self._registry,
            'columns': self.columns,
            'rows': self.rows,
            'column_ratio': 100 - self.columns * 2,
            'row_ratio': 100 - self.rows * 2,
        }
        context.update(kwargs)
        return context

class AnalyticsDashboardView(AnalyticsView):
    pass

dashboard = AnalyticsDashboardView()


import sys

from django.core.management.base import BaseCommand
from optparse import make_option

class Command(BaseCommand):
    option_list = BaseCommand.option_list + (
        make_option('--list', action='store_true', dest='show_list', default=False,
                    help='List model items waiting to be published (limited to 100)'),
    )

    args = '[modelname] [pk] [--list]'
    help = 'Publish a specific model or models within app'
    usage_str = 'Usage: ./manage.py publish_model app.models.Blog'

    def error(self, message, code=1):
        """
        Print error and stop command
        """
        print >>sys.stderr, message
        sys.exit(code)

    def handle(self, model_name=None, pk=None, show_list=None, *args, **options):
        if not model_name:
            self.error('You must provide an app to publish.\n' + self.usage_str)

        module = self.get_model(model_name)

        # TODO: Validate model is a publisher instance

        qs = module.objects.filter(publisher_is_draft=True).filter(publisher_linked_id=None)

        if pk:
            qs = qs.filter(pk=pk)

        if qs.count() < 1:
            self.error('No model(s) found to publish')

        for model in qs.all():
            model.publish()
            print 'Successfully published %s' % model

    def get_model(self, model_name):
        """
        TODO: Need to validate model name has 2x '.' chars
        """
        klass = None
        try:
            module_name, class_name = model_name.rsplit('.', 1)
            mod = __import__(module_name, fromlist=[class_name])
            klass = getattr(mod, class_name)
        except ImportError, e:
            self.error('Cannot find app %s %s' % (model_name, e))

        return klass


""" Error responses """
from aiohttp.web import Response
import json

class JResponse(Response):
    """ Modified Reponse from aohttp """
    def __init__(self, *, status=200, reason=None,
                 headers=None, jsonrpc=None):
        if jsonrpc is not None:
            jsonrpc.update({'jsonrpc': '2.0'})
            text = json.dumps(jsonrpc)
        super().__init__(status=status, reason=reason, text=text,
                         headers=headers, content_type='application/json')

class JError(object):
    """ Class with standart errors """
    def __init__(self, data=None, rid=None):
        if data is not None:
            self.rid = data['id']
        else:
            self.rid = rid

    def parse(self):
        """ json parsing error """
        return JResponse(jsonrpc={
            'id': self.rid,
            'error': {'code': -32700, 'message': 'Parse error'},
        })

    def request(self):
        """ incorrect json rpc request """
        return JResponse(jsonrpc={
            'id': self.rid,
            'error': {'code': -32600, 'message': 'Invalid Request'},
        })

    def method(self):
        """ Not found method on the server """
        return JResponse(jsonrpc={
            'id': self.rid,
            'error': {'code': -32601, 'message': 'Method not found'},
        })

    def params(self):
        """ Incorrect params (used in validate) """
        return JResponse(jsonrpc={
            'id': self.rid,
            'error': {'code': -32602, 'message': 'Invalid params'},
        })

    def internal(self):
        """ Internal server error, actually send on every unknow exception """
        return JResponse(jsonrpc={
            'id': self.rid,
            'error': {'code': -32603, 'message': 'Internal error'},
        })

    def custom(self, code, message):
        """
        Specific server side errors use: -32000 to -32099
        reserved for implementation-defined server-errors
        """
        if -32000 < code or -32099 > code:
            code = -32603
            message = 'Internal error'
        return JResponse(jsonrpc={
            'id': self.rid,
            'error': {'code': code, 'message': message},
        })


""" Simple JSON-RPC 2.0 protocol for aiohttp"""
from .exc import (ParseError, InvalidRequest, InvalidParams,
                  InternalError, InvalidResponse)
from .errors import JError, JResponse

from validictory import validate, ValidationError, SchemaError
from functools import wraps
from uuid import uuid4
from aiohttp import ClientSession
import asyncio
import json
import traceback

__version__ = '0.1.0'

REQ_JSONRPC20 = {
    "type": "object",
    "properties": {
        "jsonrpc": {"pattern": r"2\.0"},
        "method": {"type": "string"},
        "params": {"type": "any"},
        "id": {"type": "any"},
    },
}
RSP_JSONRPC20 = {
    "type": "object",
    "properties": {
        "jsonrpc": {"pattern": r"2\.0"},
        "result": {"type": "any"},
        "id": {"type": "any"},
    },
}
ERR_JSONRPC20 = {
    "type": "object",
    "properties": {
        "jsonrpc": {"pattern": r"2\.0"},
        "error": {
            "type": "object",
            "properties": {
                "code": {"type": "number"},
                "message": {"type": "string"},
            }
        },
        "id": {"type": "any"},
    },
}

@asyncio.coroutine
def jrpc_errorhandler_middleware(app, handler):
    @asyncio.coroutine
    def middleware(request):
        try:
            return (yield from handler(request))
        except Exception:
            traceback.print_exc()
            return JError().internal()
    return middleware

@asyncio.coroutine
def decode(request):
    """ Get/decode/validate json from request """
    try:
        data = yield from request.json(loader=json.loads)
    except Exception as err:
        raise ParseError(err)

    try:
        validate(data, REQ_JSONRPC20)
    except ValidationError as err:
        raise InvalidRequest(err)
    except SchemaError as err:
        raise InternalError(err)
    except Exception as err:
        raise InternalError(err)
    return data

class Service(object):
    """ Service class """

    def __new__(cls, ctx):
        """ Return on call class """
        return cls.__run(cls, ctx)

    def valid(schema=None):
        """ Validation data by specific validictory configuration """
        def dec(fun):
            @wraps(fun)
            def d_func(self, ctx, data, *a, **kw):
                try:
                    validate(data['params'], schema)
                except ValidationError as err:
                    raise InvalidParams(err)
                except SchemaError as err:
                    raise InternalError(err)
                return fun(self, ctx, data['params'], *a, **kw)
            return d_func
        return dec

    @asyncio.coroutine
    def __run(self, ctx):
        """ Run service """
        try:
            data = yield from decode(ctx)
        except ParseError:
            return JError().parse()
        except InvalidRequest:
            return JError().request()
        except InternalError:
            return JError().internal()

        try:
            i_app = getattr(self, data['method'])
            i_app = asyncio.coroutine(i_app)
        except Exception:
            return JError(data).method()

        try:
            resp = yield from i_app(self, ctx, data)
        except InvalidParams:
            return JError(data).params()
        except InternalError:
            return JError(data).internal()

        return JResponse(jsonrpc={
            "id": data['id'], "result": resp
            })

class Response(object):

    __slots__ = ['id', 'error', 'result']

    def __init__(self, id, result=None, error=None, **kw):
        self.id = id
        self.result = result
        self.error = error

    def __repr__(self):
        return "Response(id={rid}, result={res}, error={err}".format(
                    rid=self.id, res=self.result, err=self.error)

class Client(object):
    def __init__(self, url, dumper=None, loop=None):
        self.url = url
        self.dumper = dumper
        if not loop:
            loop = asyncio.get_event_loop()
        if not self.dumper:
            self.dumper = json.dumps

        self.client = ClientSession(
                          loop=loop,
                          headers={'content-type': 'application/json'})

    def __del__(self):
        self.client.close()

    def __encode(self, method, params=None, id=None):
        try:
            data = self.dumper({
                    "jsonrpc": "2.0",
                    "id": id,
                    "method": method,
                    "params": params
                   })
        except Exception as e:
            raise Exception("Can not encode: {}".format(e))

        return data

    @asyncio.coroutine
    def call(self, method, params=None, id=None, schem=None):
        if not id:
            id = uuid4().hex
        try:
            resp = yield from self.client.post(
                   self.url, data=self.__encode(method, params, id))
        except Exception as err:
            raise Exception(err)

        if 200 != resp.status:
            raise InvalidResponse(
                "Error, server retunrned: {status}".format(status=resp.status))

        try:
            data = yield from resp.json()
        except Exception as err:
            raise InvalidResponse(err)

        try:
            validate(data, ERR_JSONRPC20)
            return Response(**data)
        except ValidationError:
            # Passing data to validate response.
            # Good if does not valid to ERR_JSONRPC20 object.
            pass
        except Exception as err:
            raise InvalidResponse(err)

        try:
            validate(data, RSP_JSONRPC20)
            if id != data['id']:
                raise InvalidResponse(
                       "Rsponse id {local} not equal {remote}".format(
                            local=id, remote=data['id']))
        except Exception as err:
            raise InvalidResponse(err)

        if schem:
            try:
                validate(data['result'], schem)
            except ValidationError as err:
                raise InvalidResponse(err)
            except Exception as err:
                raise InternalError(err)

        return Response(**data)


#_MIT License
#_
#_Copyright (c) 2017 Dan Persons (dpersonsdev@gmail.com)
#_
#_Permission is hereby granted, free of charge, to any person obtaining a copy
#_of this software and associated documentation files (the "Software"), to deal
#_in the Software without restriction, including without limitation the rights
#_to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#_copies of the Software, and to permit persons to whom the Software is
#_furnished to do so, subject to the following conditions:
#_
#_The above copyright notice and this permission notice shall be included in all
#_copies or substantial portions of the Software.
#_
#_THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#_IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#_FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#_AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#_LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#_OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#_SOFTWARE.

# lightcli
# A lightweight terminal interaction library for Python.

import os
import sys
import time

__version__ = '1.1'

def string_input(prompt=''):
    """Python 3 input()/Python 2 raw_input()"""
    v = sys.version[0]
    if v == '3':
        return input(prompt)
    else:
        return raw_input(prompt)

def choice_input(options=[], prompt='Press ENTER to continue.',
        showopts=True, qopt=False):
    """Get input from a list of choices (q to quit)"""

    choice = None
    if showopts:
        prompt = prompt + ' ' + str(options)
    if qopt:
        prompt = prompt + ' (q to quit)'

    while not choice:
        try:
            choice = string_input(prompt + ' ')
        except SyntaxError:
            if options == []:
                pass
        if choice:
            if choice in options:
                return choice
            elif qopt == True and choice == 'q':
                choice = None
                is_sure = string_input('Are you sure you want to quit? ')
                if is_sure in ('Y', 'y', 'yes'):
                    exit('\nThanks for playing. Goodbye.\n')
            elif options == []:
                return 0
            else:
                print('Answer must be one of ' + str(options) +
                        '. Your answer?')
                if options:
                    choice = None
        elif options == []:
            return 0
        else:
            print('Answer must be one of ' + str(options) +
                    '. Your answer?')

def long_input(prompt='Multi-line input\n' + \
        'Enter EOF on a blank line to end ' + \
        '(ctrl-D in *nix, ctrl-Z in windows)',
        maxlines = None, maxlength = None):
    """Get a multi-line string as input"""

    lines = []
    print(prompt)
    lnum = 1

    try:
        while True:

            if maxlines:

                if lnum > maxlines:
                    break

                else:
                    if maxlength:
                        lines.append(string_input('')[:maxlength])
                    else:
                        lines.append(string_input(''))
                    lnum += 1

            else:
                if maxlength:
                    lines.append(string_input('')[:maxlength])
                else:
                    lines.append(string_input(''))

    except EOFError:
        pass
    finally:
        return '\n'.join(lines)

def list_input(prompt='List input - enter each item on a seperate line\n' + \
        'Enter EOF on a blank line to end ' + \
        '(ctrl-D in *nix, ctrl-Z in windows)',
        maxitems=None, maxlength=None):
    """Get a list of strings as input"""

    lines = []
    print(prompt)
    inum = 1

    try:

        while True:

            if maxitems:

                if inum > maxitems:
                    break
                else:
                    if maxlength:
                        lines.append(string_input('')[:maxlength])
                    else:
                        lines.append(string_input(''))
                    inum += 1

            else:
                if maxlength:
                    lines.append(string_input('')[:maxlength])
                else:
                    lines.append(string_input(''))

    except EOFError:
        pass
    finally:
        return lines

def outfile_input(extension=None):
    """Get an output file name as input"""

    fileok = False

    while not fileok:
        filename = string_input('File name? ')
        if extension:
            if not filename.endswith(extension):
                if extension.startswith('.'):
                    filename = filename + extension
                else:
                    filename = filename + '.' + extension
        if os.path.isfile(filename):
            choice = choice_input(prompt=filename + \
                    ' already exists. Overwrite?',
                    options=['y', 'n'])
            if choice == 'y':
                try:
                    nowtime = time.time()
                    with open(filename, 'a') as f:
                        os.utime(filename, (nowtime, nowtime))
                    fileok = True
                except IOError:
                    print('Write permission denied on ' + filename + \
                            '. Try again.')
                except PermissionError:
                    print('Write permission denied on ' + filename + \
                            '. Try again.')
                except FileNotFoundError:
                    print(filename + ': directory not found. Try again.')

        else:
            choice = choice_input(
                    prompt=filename + ' does not exist. Create it?',
                    options=['y', 'n'])
            if choice == 'y':
                try:
                    nowtime = time.time()
                    with open(filename, 'w') as f:
                        os.utime(filename, (nowtime, nowtime))
                    fileok = True
                except IOError:
                    print('Write permission denied on ' + filename + \
                            '. Try again.')
                except PermissionError:
                    print('Write permission denied on ' + filename + \
                            '. Try again.')
                except FileNotFoundError:
                    print(filename + ': directory not found. Try again.')

    return filename


import future
import future.utils

import numpy as np
from pyquery import PyQuery as pq

import sportsref

class Team(future.utils.with_metaclass(sportsref.decorators.Cached, object)):

    def __init__(self, team_id):
        self.team_id = team_id.upper()

    def __eq__(self, other):
        return (self.team_id == other.team_id)

    def __hash__(self):
        return hash(self.team_id)

    @sportsref.decorators.memoize
    def team_year_url(self, yr_str):
        return (sportsref.nba.BASE_URL +
                '/teams/{}/{}.htm'.format(self.team_id, yr_str))

    @sportsref.decorators.memoize
    def get_main_doc(self):
        relURL = '/teams/{}'.format(self.team_id)
        teamURL = sportsref.nba.BASE_URL + relURL
        mainDoc = pq(sportsref.utils.get_html(teamURL))
        return mainDoc

    @sportsref.decorators.memoize
    def get_year_doc(self, yr_str):
        return pq(sportsref.utils.get_html(self.team_year_url(yr_str)))

    @sportsref.decorators.memoize
    def name(self):
        """Returns the real name of the franchise given the team ID.

        Examples:
        'BOS' -> 'Boston Celtics'
        'NJN' -> 'Brooklyn Nets'

        :returns: A string corresponding to the team's full name.
        """
        doc = self.get_main_doc()
        name = doc('div#info h1[itemprop="name"]').text()
        return name

    @sportsref.decorators.memoize
    def roster(self, year):
        """Returns the roster table for the given year.

        :year: The year for which we want the roster; defaults to current year.
        :returns: A DataFrame containing roster information for that year.
        """
        doc = self.get_year_doc(year)
        table = doc('table#roster')
        df = sportsref.utils.parse_table(table)
        df['years_experience'] = (df['years_experience']
                                  .replace('R', 0).replace('', np.nan).astype(float))
        return df

    # TODO: kind_rpb
    @sportsref.decorators.memoize
    def schedule(self, year):
        """Gets schedule information for a team-season.

        :year: The year for which we want the schedule.
        :returns: DataFrame of schedule information.
        """
        doc = self.get_year_doc('{}_games'.format(year))
        table = doc('table#games')
        df = sportsref.utils.parse_table(table)
        return df