import hashlib
import mimetypes
from copy import copy
from datetime import datetime
from io import BytesIO

from django.template.defaultfilters import filesizeformat
from django.urls import reverse
from django.utils.translation import gettext as _

import magic
from couchdbkit.exceptions import ResourceConflict, ResourceNotFound
from lxml import etree
from memoized import memoized
from PIL import Image

from dimagi.ext.couchdbkit import (
    DateTimeProperty,
    DictProperty,
    Document,
    DocumentSchema,
    IntegerProperty,
    SafeSaveDocument,
    SchemaDictProperty,
    SchemaListProperty,
    StringListProperty,
    StringProperty,
)
from dimagi.utils.couch.database import get_safe_read_kwargs, iter_docs
from dimagi.utils.couch.resource_conflict import retry_resource

from corehq import privileges, toggles
from corehq.apps.accounting.utils import domain_has_privilege
from corehq.apps.app_manager.exceptions import XFormException
from corehq.apps.app_manager.templatetags.xforms_extras import clean_trans
from corehq.apps.app_manager.xform import XFormValidationError
from corehq.apps.domain import SHARED_DOMAIN
from corehq.apps.domain.models import LICENSE_LINKS, LICENSES
from corehq.apps.hqmedia.exceptions import BadMediaFileException
from corehq.blobs.mixin import CODES, BlobMixin

MULTIMEDIA_PREFIX = "jr://file/"
LOGO_ARCHIVE_KEY = 'logos'

class AuxMedia(DocumentSchema):
    """
    Additional metadata companion for couch models
    that you want to add arbitrary attachments to
    """
    uploaded_date = DateTimeProperty()
    uploaded_by = StringProperty()
    uploaded_filename = StringProperty()  # the uploaded filename info
    checksum = StringProperty()
    attachment_id = StringProperty()  # the actual attachment id in blobs
    media_meta = DictProperty()
    notes = StringProperty()

class HQMediaLicense(DocumentSchema):
    domain = StringProperty()
    author = StringProperty()
    organization = StringProperty()
    type = StringProperty(choices=LICENSES)
    attribution_notes = StringProperty()

    def __init__(self, _d=None, **properties):
        # another place we have to lazy migrate
        if properties and properties.get('type', '') == 'public':
            properties['type'] = 'cc'
        super(HQMediaLicense, self).__init__(_d, **properties)

    @property
    def display_name(self):
        return LICENSES.get(self.type, "Improper License")

    @property
    def deed_link(self):
        return LICENSE_LINKS.get(self.type)

class CommCareMultimedia(BlobMixin, SafeSaveDocument):
    """
    The base object of all CommCare Multimedia
    """
    file_hash = StringProperty()  # use this to search for multimedia in couch
    aux_media = SchemaListProperty(AuxMedia)

    last_modified = DateTimeProperty()
    valid_domains = StringListProperty()  # A list of domains that uses this file
    owners = StringListProperty(default=[])  # list of domains that uploaded this file
    licenses = SchemaListProperty(HQMediaLicense, default=[])
    shared_by = StringListProperty(default=[])  # list of domains that can share this file
    tags = DictProperty(default={})  # dict of string lists
    _blobdb_type_code = CODES.multimedia

    @classmethod
    def get(cls, docid, rev=None, db=None, dynamic_properties=True):
        # copied and tweaked from the superclass's method
        if not db:
            db = cls.get_db()
        cls._allow_dynamic_properties = dynamic_properties
        # on cloudant don't get the doc back until all nodes agree
        # on the copy, to avoid race conditions
        extras = get_safe_read_kwargs()
        return db.get(docid, rev=rev, wrapper=cls.wrap, **extras)

    @property
    def is_shared(self):
        return len(self.shared_by) > 0

    @property
    def license(self):
        return self.licenses[0] if self.licenses else None

    @retry_resource(3)
    def update_or_add_license(self, domain, type="", author="", attribution_notes="", org="", should_save=True):
        for license in self.licenses:
            if license.domain == domain:
                license.type = type or license.type
                license.author = author or license.author
                license.organization = org or license.organization
                license.attribution_notes = attribution_notes or license.attribution_notes
                break
        else:
            license = HQMediaLicense(domain=domain, type=type, author=author,
                                     attribution_notes=attribution_notes, organization=org)
            self.licenses.append(license)

        if should_save:
            self.save()

    def url(self):
        return reverse("hqmedia_download", args=[self.doc_type, self._id])

    def attach_data(self, data, original_filename=None, username=None, attachment_id=None,
                    media_meta=None):
        """
        This creates the auxmedia attachment with the downloaded data.
        """
        self.last_modified = datetime.utcnow()

        if not attachment_id:
            attachment_id = self.file_hash

        if not self.blobs or attachment_id not in self.blobs:
            if not getattr(self, '_id'):
                # put_attchment blows away existing data, so make sure an id has been assigned
                # to this guy before we do it. This is the usual path; remote apps are the exception.
                self.save()
            self.put_attachment(
                data,
                attachment_id,
                content_type=self.get_mime_type(data, filename=original_filename),
                domain=SHARED_DOMAIN,
            )
        new_media = AuxMedia()
        new_media.uploaded_date = datetime.utcnow()
        new_media.attachment_id = attachment_id
        new_media.uploaded_filename = original_filename
        new_media.uploaded_by = username
        new_media.checksum = self.file_hash
        if media_meta:
            new_media.media_meta = media_meta
        self.aux_media.append(new_media)
        self.save()
        return True

    def add_domain(self, domain, owner=None, **kwargs):
        if len(self.owners) == 0:
            # this is intended to simulate migration--if it happens that a media file somehow gets no more owners
            # (which should be impossible) it will transfer ownership to all copiers... not necessarily a bad
            # thing, just something to be aware of
            self.owners = self.valid_domains

        if owner and domain not in self.owners:
            self.owners.append(domain)
        elif not owner and domain in self.owners:
            self.owners.remove(domain)

        if domain in self.owners:
            shared = kwargs.get('shared', '')
            if shared and domain not in self.shared_by:
                self.shared_by.append(domain)
            elif not shared and shared != '' and domain in self.shared_by:
                self.shared_by.remove(domain)

            if kwargs.get('tags', ''):
                self.tags[domain] = kwargs['tags']

        if domain not in self.valid_domains:
            self.valid_domains.append(domain)
        self.save()

    def get_display_file(self, return_type=True):
        if self.attachment_id:
            data = self.fetch_attachment(self.attachment_id, stream=True).read()
            if return_type:
                content_type = self.blobs[self.attachment_id].content_type
                return data, content_type
            else:
                return data
        return None

    def get_file_extension(self):
        extension = ''
        if self.aux_media:
            extension = '.{}'.format(self.aux_media[-1]['uploaded_filename'].split(".")[-1])
        return extension

    def get_media_info(self, path, is_updated=False, original_path=None):
        return {
            "path": path,
            "uid": self.file_hash,
            "m_id": self._id,
            "url": reverse("hqmedia_download", args=[self.__class__.__name__, self._id]),
            "updated": is_updated,
            "original_path": original_path,
            "icon_class": self.get_icon_class(),
            "media_type": self.get_nice_name(),
            "humanized_content_length": filesizeformat(self.content_length),
        }

    @property
    def attachment_id(self):
        if not self.aux_media:
            return None
        ids = set([aux.attachment_id for aux in self.aux_media])
        assert len(ids) == 1
        return ids.pop()

    @property
    def content_length(self):
        if self.attachment_id:
            return self.blobs[self.attachment_id].content_length

    @classmethod
    def get_mime_type(cls, data, filename=None):
        mime = magic.Magic(mime=True)
        mime_type = mime.from_buffer(data)
        if mime_type.startswith('application') and filename is not None:
            guessed_type = mimetypes.guess_type(filename)
            mime_type = guessed_type[0] if guessed_type[0] else mime_type
        return mime_type

    @classmethod
    def get_base_mime_type(cls, data, filename=None):
        mime_type = cls.get_mime_type(data, filename=filename)
        return mime_type.split('/')[0] if mime_type else None

    @classmethod
    def generate_hash(cls, data):
        return hashlib.md5(data).hexdigest()

    @classmethod
    def get_by_hash(cls, file_hash):
        result = cls.view('hqmedia/by_hash', key=file_hash, include_docs=True).first()
        if not result:
            result = cls()
            result.file_hash = file_hash
        return result

    @classmethod
    def get_by_data(cls, data):
        file_hash = cls.generate_hash(data)
        return cls.get_by_hash(file_hash)

    @classmethod
    def get_doc_types(cls):
        return ('CommCareImage', 'CommCareAudio', 'CommCareVideo')

    @classmethod
    def get_doc_class(cls, doc_type):
        return {
            'CommCareImage': CommCareImage,
            'CommCareAudio': CommCareAudio,
            'CommCareVideo': CommCareVideo,
            'CommCareMultimedia': CommCareMultimedia,
        }[doc_type]

    @classmethod
    def get_class_by_data(cls, data, filename=None):
        return {
            'image': CommCareImage,
            'audio': CommCareAudio,
            'video': CommCareVideo,
        }.get(cls.get_base_mime_type(data, filename=filename))

    @classmethod
    def get_form_path(cls, path, lowercase=False):
        path = path.strip()
        if lowercase:
            path = path.lower()
        if path.startswith(MULTIMEDIA_PREFIX):
            return path
        if path.startswith('/'):
            path = path[1:]
        return "%s%s" % (MULTIMEDIA_PREFIX, path)

    @classmethod
    def get_standard_path(cls, path):
        return path.replace(MULTIMEDIA_PREFIX, "")

    @classmethod
    def wrap(cls, data):
        should_save = False
        if data.get('tags') == []:
            data['tags'] = {}
        if not data.get('owners'):
            data['owners'] = data.get('valid_domains', [])
        if isinstance(data.get('licenses', ''), dict):
            # need to migrate licncses from old format to new format
            # old: {"mydomain": "public", "yourdomain": "cc"}
            migrated = [HQMediaLicense(domain=domain, type=type)._doc
                        for domain, type in data["licenses"].items()]
            data['licenses'] = migrated

        # deprecating support for public domain license
        if isinstance(data.get("licenses", ""), list) and len(data["licenses"]) > 0:
            if data["licenses"][0].get("type", "") == "public":
                data["licenses"][0]["type"] = "cc"
                should_save = True
        self = super(CommCareMultimedia, cls).wrap(data)
        if should_save:
            self.save()
        return self

    @classmethod
    def get_nice_name(cls):
        return _("Generic Multimedia")

    @classmethod
    def get_icon_class(cls):
        return "fa fa-desktop"

class ImageThumbnailError(Exception):
    pass

class CommCareImage(CommCareMultimedia):

    class Config(object):
        search_view = 'hqmedia/image_search'

    def attach_data(self, data, original_filename=None, username=None, attachment_id=None, media_meta=None):
        image = self.get_image_object(data)
        attachment_id = "%dx%d" % image.size
        attachment_id = "%s-%s.%s" % (self.file_hash, attachment_id, image.format)
        if not media_meta:
            media_meta = {}
        media_meta["size"] = {
            "width": image.size[0],
            "height": image.size[1]
        }
        return super(CommCareImage, self).attach_data(data, original_filename=original_filename, username=username,
                                                      attachment_id=attachment_id, media_meta=media_meta)

    def get_media_info(self, path, is_updated=False, original_path=None):
        info = super(CommCareImage, self).get_media_info(path, is_updated=is_updated, original_path=original_path)
        info['image_size'] = self.image_size_display()
        return info

    def image_size_display(self):
        for aux_media in self.aux_media:
            image_size = aux_media.media_meta.get('size', {})
            width = image_size.get('width')
            height = image_size.get('height')
            if width is not None and height is not None:
                return "{} x {} px".format(width, height)

    @classmethod
    def get_image_object(cls, data):
        try:
            return Image.open(BytesIO(data))
        except IOError:
            raise BadMediaFileException(_('Upload is not a valid image file.'))

    @classmethod
    def _get_resized_image(cls, image, size):
        if image.mode not in ["RGB", "RGBA"]:
            image = image.convert("RGB")
        o = BytesIO()
        try:
            image.thumbnail(size, Image.Resampling.LANCZOS)
        except IndexError:
            raise ImageThumbnailError()
        image.save(o, format="PNG")
        return o.getvalue()

    @classmethod
    def get_invalid_image_data(cls):
        import os
        invalid_image_path = os.path.join(os.path.dirname(__file__), 'static/hqmedia/images/invalid_image.png')
        return Image.open(open(invalid_image_path, 'rb'))

    @classmethod
    def get_thumbnail_data(cls, data, size):
        try:
            data = cls._get_resized_image(cls.get_image_object(data), size)
        except (ImageThumbnailError, ImportError, IOError):
            data = cls._get_resized_image(cls.get_invalid_image_data(), size)
        return data

    @classmethod
    def get_nice_name(cls):
        return _("Image")

    @classmethod
    def get_icon_class(cls):
        return "fa fa-picture-o"

class CommCareAudio(CommCareMultimedia):

    class Config(object):
        search_view = 'hqmedia/audio_search'

    @classmethod
    def get_nice_name(cls):
        return _("Audio")

    @classmethod
    def get_icon_class(cls):
        return "fa fa-volume-up"

class CommCareVideo(CommCareMultimedia):

    @classmethod
    def get_nice_name(cls):
        return _("Video")

    @classmethod
    def get_icon_class(cls):
        return "fa fa-video-camera"

class HQMediaMapItem(DocumentSchema):

    multimedia_id = StringProperty()
    media_type = StringProperty()
    version = IntegerProperty()
    unique_id = StringProperty()
    upstream_media_id = StringProperty()

    @property
    def url(self):
        return reverse("hqmedia_download", args=[self.media_type, self.multimedia_id]) \
            if self.multimedia_id else ""

    @classmethod
    def gen_unique_id(cls, m_id, path):
        return hashlib.md5(b"%s: %s" % (path.encode('utf-8'), m_id.encode('utf-8'))).hexdigest()

class ApplicationMediaReference(object):
    """
        Generated when all_media is queried and stored in memory.

        Contains all the information needed to know where / how this the media PATH is stored in the application.
        Provides no link to a multimedia object---link comes from the application's multimedia map.
        Useful info for user-facing things.
    """

    def __init__(self, path, module_unique_id=None, module_name=None,
                 form_unique_id=None, form_name=None, form_order=None, media_class=None,
                 is_menu_media=False, app_lang=None, use_default_media=False):

        if not isinstance(path, str):
            path = ''
        self.path = path.strip()

        if not issubclass(media_class, CommCareMultimedia):
            raise ValueError("media_class should be a type of CommCareMultimedia")

        self.module_unique_id = module_unique_id
        self.module_name = module_name

        self.form_unique_id = form_unique_id
        self.form_name = form_name
        self.form_order = form_order

        self.media_class = media_class
        self.is_menu_media = is_menu_media

        self.app_lang = app_lang or "en"

        self.use_default_media = use_default_media

    def __str__(self):
        detailed_location = ""
        if self.module_name:
            detailed_location = " | %s" % self.get_module_name()
        if self.form_name:
            detailed_location = "%s > %s" % (detailed_location, self.get_form_name())
        return "%(media_class)s at %(path)s%(detailed_location)s" % {
            'media_class': self.media_class.__name__,
            'path': self.path,
            'detailed_location': detailed_location,
        }

    def as_dict(self):
        return {
            'module': {
                'name': self.get_module_name(),
                'unique_id': self.module_unique_id,
            },
            'form': {
                'name': self.get_form_name(),
                'unique_id': self.form_unique_id,
                'order': self.form_order,
            },
            'is_menu_media': self.is_menu_media,
            'media_class': self.media_class.__name__,
            'path': self.path,
            "icon_class": self.media_class.get_icon_class(),
            "media_type": self.media_class.get_nice_name(),
            "use_default_media": self.use_default_media,
        }

    def _get_name(self, raw_name):
        if not raw_name:
            return ""
        if not isinstance(raw_name, dict):
            return raw_name
        return raw_name.get(self.app_lang, list(raw_name.values())[0])

    def get_module_name(self):
        return self._get_name(self.module_name)

    def get_form_name(self):
        return self._get_name(self.form_name)

class MediaMixin(object):
    """
        An object that has multimedia associated with it.
        Used by apps, modules, forms.
    """
    def get_media_ref_kwargs(self):
        raise NotImplementedError

    def all_media(self, lang=None):
        """
            Finds all the multimedia IMAGES and AUDIO referenced in this object
            Returns list of ApplicationMediaReference objects
        """
        raise NotImplementedError

    def get_references(self, lang=None):
        """
            Used for the multimedia controller.
        """
        return [m.as_dict() for m in self.all_media(lang=lang)]

    def get_relevant_multimedia_map(self, app):
        references = self.get_references()
        return {r['path']: app.multimedia_map[r['path']]
                for r in references if r['path'] in app.multimedia_map}

    def rename_media(self, old_path, new_path):
        """
            Returns a count of number of changes made.
            Should rename each item returned by all_media.
        """
        raise NotImplementedError

    @memoized
    def all_media_paths(self, lang=None):
        return set([m.path for m in self.all_media(lang=lang)])

    @memoized
    def get_all_paths_of_type(self, media_type):
        return set([m.path for m in self.all_media() if m.media_class.__name__ == media_type])

    def menu_media(self, menu, lang=None):
        """
            Convenience method. Gets the ApplicationMediaReference for a menu that's
            associated with this MediaMixin (which may be self or a different attribute)

            Note that "menu" may refer to any object that implements NavMenuItemMediaMixin,
            like a module or a form.
        """
        kwargs = self.get_media_ref_kwargs()
        media = []
        media.extend([ApplicationMediaReference(image, media_class=CommCareImage, is_menu_media=True, **kwargs)
                      for image in menu.all_image_paths(lang=lang) if image])
        media.extend([ApplicationMediaReference(audio, media_class=CommCareAudio, is_menu_media=True, **kwargs)
                      for audio in menu.all_audio_paths(lang=lang) if audio])
        return media

    def rename_menu_media(self, menu, old_path, new_path):
        """
            Convenience method, similar to menu_media.
        """
        app = self.get_app()
        update_count = 0
        for lang in app.langs:
            if menu.icon_by_language(lang) == old_path:
                menu.set_icon(lang, new_path)
                update_count += 1
            if menu.audio_by_language(lang) == old_path:
                menu.set_audio(lang, new_path)
                update_count += 1
        return update_count

class ModuleMediaMixin(MediaMixin):
    def get_media_ref_kwargs(self):
        return {
            'app_lang': self.get_app().default_language,
            'module_name': self.name,
            'module_unique_id': self.unique_id,
            'form_name': None,
            'form_unique_id': None,
            'form_order': None,
        }

    def all_media(self, lang=None):
        kwargs = self.get_media_ref_kwargs()
        media = []

        media.extend(self.menu_media(self, lang=lang))

        # Registration from case list
        if self.case_list_form.form_id:
            media.extend(self.menu_media(self.case_list_form, lang=lang))

        # Case list menu item
        if hasattr(self, 'case_list') and self.case_list.show:
            media.extend(self.menu_media(self.case_list, lang=lang))

        # Case search and claim menu items
        if hasattr(self, 'search_config'):
            media.extend(self.menu_media(self.search_config.search_label, lang=lang))
            media.extend(self.menu_media(self.search_config.search_again_label, lang=lang))

        for name, details, display in self.get_details():
            # Case list lookup - not language-specific
            if display and details.display == 'short' and details.lookup_enabled and details.lookup_image:
                media.append(ApplicationMediaReference(details.lookup_image, media_class=CommCareImage,
                                                       is_menu_media=True, **kwargs))

            # Print template - not language-specific
            if display and details.display == 'long' and details.print_template:
                media.append(ApplicationMediaReference(details.print_template['path'],
                                                       media_class=CommCareMultimedia, **kwargs))

            # Icon-formatted columns
            for column in details.get_columns():
                if column.format == 'enum-image' or column.format == 'clickable-icon':
                    for map_item in column.enum:
                        icon = clean_trans(map_item.value, [lang] + self.get_app().langs)
                        if icon:
                            media.append(ApplicationMediaReference(icon, media_class=CommCareImage,
                                                                   is_menu_media=True, **kwargs))

        return media

    def rename_media(self, old_path, new_path):
        count = 0

        count += self.rename_menu_media(self, old_path, new_path)

        # Registration from case list
        if self.case_list_form.form_id:
            count += self.rename_menu_media(self.case_list_form, old_path, new_path)

        # Case list menu item
        if hasattr(self, 'case_list') and self.case_list.show:
            count += self.rename_menu_media(self.case_list, old_path, new_path)

        # Case search and claim menu items
        if hasattr(self, 'search_config'):
            count += self.rename_menu_media(self.search_config.search_label, old_path, new_path)
            count += self.rename_menu_media(self.search_config.search_again_label, old_path, new_path)

        for name, details, display in self.get_details():
            # Case list lookup
            if display and details.display == 'short' and details.lookup_enabled and details.lookup_image:
                if details.lookup_image == old_path:
                    details.lookup_image = new_path
                    count += 1

            # Print template
            if display and details.display == 'long' and details.print_template:
                details.print_template['path'] = new_path
                count += 1

            # Icon-formatted columns
            for column in details.get_columns():
                if column.format == 'enum-image':
                    for map_item in column.enum:
                        for lang, icon in map_item.value.items():
                            if icon == old_path:
                                map_item.value[lang] = new_path
                                count += 1

        return count

class FormMediaMixin(MediaMixin):
    def get_media_ref_kwargs(self):
        module = self.get_module()
        return {
            'app_lang': module.get_app().default_language,
            'module_name': module.name,
            'module_unique_id': module.unique_id,
            'form_name': self.name,
            'form_unique_id': self.unique_id,
            'form_order': self.id,
        }

    @memoized
    def memoized_xform(self):
        return self.wrapped_xform()

    def all_media(self, lang=None):
        kwargs = self.get_media_ref_kwargs()

        media = copy(self.menu_media(self, lang=lang))

        # Form questions
        try:
            parsed = self.wrapped_xform()
            if parsed.exists():
                self.validate_form()
            else:
                return media
        except (XFormValidationError, XFormException):
            return media

        for image in parsed.image_references(lang=lang):
            if image:
                media.append(ApplicationMediaReference(image, media_class=CommCareImage, **kwargs))
        for audio in parsed.audio_references(lang=lang):
            if audio:
                media.append(ApplicationMediaReference(audio, media_class=CommCareAudio, **kwargs))
        for video in parsed.video_references(lang=lang):
            if video:
                media.append(ApplicationMediaReference(video, media_class=CommCareVideo, **kwargs))
        for text in parsed.text_references(lang=lang):
            if text:
                media.append(ApplicationMediaReference(text, media_class=CommCareMultimedia, **kwargs))

        return media

    def rename_media(self, old_path, new_path):
        menu_count = self.rename_menu_media(self, old_path, new_path)

        xform_count = 0
        if self.form_type != 'shadow_form':
            xform_count = self.memoized_xform().rename_media(old_path, new_path)
            if xform_count:
                self.source = etree.tostring(self.memoized_xform().xml, encoding='utf-8').decode('utf-8')

        return menu_count + xform_count

class ApplicationMediaMixin(Document, MediaMixin):
    """
        Manages multimedia for itself and sub-objects.
    """

    # keys are the paths to each file in the final application media zip
    multimedia_map = SchemaDictProperty(HQMediaMapItem)

    # paths to custom logos
    logo_refs = DictProperty()

    # where we store references to the old logos (or other multimedia) on a downgrade,
    # so that information is not lost
    archived_media = DictProperty()

    @memoized
    def all_media(self, lang=None):
        """
            Somewhat counterituitively, this contains all media in the app EXCEPT app-level media (logos).
        """
        media = []
        self.media_form_errors = False

        for module in [m for m in self.get_modules() if m.uses_media()]:
            media.extend(module.all_media(lang=lang))
            for form in module.get_forms():
                try:
                    form.validate_form()
                except (XFormValidationError, XFormException):
                    self.media_form_errors = True
                else:
                    media.extend(form.all_media(lang=lang))
        return media

    def multimedia_map_for_build(self, build_profile=None, remove_unused=False):
        if self.multimedia_map is None:
            self.multimedia_map = {}

        if self.multimedia_map and remove_unused:
            self.remove_unused_mappings()

        if not build_profile or not domain_has_privilege(self.domain, privileges.BUILD_PROFILES):
            return self.multimedia_map

        requested_media = copy(self.logo_paths)   # logos aren't language-specific
        for lang in build_profile.langs:
            requested_media |= self.all_media_paths(lang=lang)

        return {path: self.multimedia_map[path] for path in requested_media if path in self.multimedia_map}

    # The following functions (get_menu_media, get_case_list_form_media, get_case_list_menu_item_media,
    # get_case_list_lookup_image, _get_item_media, and get_media_ref_kwargs) are used to set up context
    # for app manager settings pages. Ideally, they'd be moved into ModuleMediaMixin and FormMediaMixin
    # and perhaps share logic with those mixins' versions of all_media.
    def get_menu_media(self, module, form=None, form_index=None, to_language=None):
        if not module:
            # user_registration isn't a real module, for instance
            return {}
        media_kwargs = self.get_media_ref_kwargs(module, form=form, form_index=form_index, is_menu_media=True)
        media_kwargs.update(to_language=to_language or self.default_language)
        item = form or module
        return self._get_item_media(item, media_kwargs)

    def get_case_list_form_media(self, module, to_language=None):
        if not module:
            # user_registration isn't a real module, for instance
            return {}
        media_kwargs = self.get_media_ref_kwargs(module)
        media_kwargs.update(to_language=to_language or self.default_language)
        return self._get_item_media(module.case_list_form, media_kwargs)

    def get_case_list_menu_item_media(self, module, to_language=None):
        if not module or not module.uses_media() or not hasattr(module, 'case_list'):
            # user_registration isn't a real module, for instance
            return {}
        media_kwargs = self.get_media_ref_kwargs(module)
        media_kwargs.update(to_language=to_language or self.default_language)
        return self._get_item_media(module.case_list, media_kwargs)

    def get_case_search_label_media(self, module, label, to_language=None):
        media_kwargs = self.get_media_ref_kwargs(module)
        media_kwargs.update(to_language=to_language or self.default_language)
        return self._get_item_media(label, media_kwargs)

    def get_case_list_lookup_image(self, module, type='case'):
        if not module:
            return {}
        media_kwargs = self.get_media_ref_kwargs(module)
        details_name = '{}_details'.format(type)
        if not hasattr(module, details_name):
            return {}

        image = ApplicationMediaReference(
            module[details_name].short.lookup_image,
            media_class=CommCareImage,
            **media_kwargs
        ).as_dict()
        return {
            'image': image
        }

    def _get_item_media(self, item, media_kwargs):
        menu_media = {}
        to_language = media_kwargs.pop('to_language', self.default_language)
        image_ref = ApplicationMediaReference(
            item.icon_by_language(to_language),
            media_class=CommCareImage,
            use_default_media=item.use_default_image_for_all,
            **media_kwargs
        )
        image_ref = image_ref.as_dict()
        menu_media['image'] = image_ref

        audio_ref = ApplicationMediaReference(
            item.audio_by_language(to_language),
            media_class=CommCareAudio,
            use_default_media=item.use_default_audio_for_all,
            **media_kwargs
        )
        audio_ref = audio_ref.as_dict()
        menu_media['audio'] = audio_ref
        return menu_media

    def get_media_ref_kwargs(self, module, form=None, form_index=None, is_menu_media=False):
        return {
            'app_lang': self.default_language,
            'module_name': module.name,
            'module_unique_id': module.unique_id,
            'form_name': form.name if form else None,
            'form_unique_id': form.unique_id if form else None,
            'form_order': form_index,
            'is_menu_media': is_menu_media,
        }

    @property
    @memoized
    def logo_paths(self):
        return set(value['path'] for value in self.logo_refs.values())

    def remove_unused_mappings(self):
        """
            This checks to see if the paths specified in the multimedia map still exist in the Application.
            If not, then that item is removed from the multimedia map.
        """
        map_changed = False
        if self.check_media_state()['has_form_errors']:
            return
        paths = list(self.multimedia_map) if self.multimedia_map else []
        permitted_paths = self.all_media_paths() | self.logo_paths
        for path in paths:
            if path not in permitted_paths:
                map_changed = True
                del self.multimedia_map[path]

        if map_changed:
            self.save()

    def create_mapping(self, multimedia, path, save=True):
        """
            This creates the mapping of a path to the multimedia in an application
            to the media object stored in couch.
        """
        path = path.strip()
        map_item = HQMediaMapItem()
        map_item.multimedia_id = multimedia._id
        map_item.unique_id = HQMediaMapItem.gen_unique_id(map_item.multimedia_id, path)
        map_item.media_type = multimedia.doc_type
        self.multimedia_map[path] = map_item

        if save:
            try:
                self.save()
            except ResourceConflict:
                # Attempt to fetch the document again.
                updated_doc = self.get(self._id)
                updated_doc.create_mapping(multimedia, path)

    def get_media_objects(self, build_profile_id=None, remove_unused=False, multimedia_map=None):
        """
            Gets all the media objects stored in the multimedia map.
            If passed a profile, will only get those that are used
            in a language in the profile.

            Returns a generator of tuples, where the first item in the tuple is
            the path (jr://...) and the second is the object (CommCareMultimedia or a subclass)
        """
        # preload all the docs to avoid excessive couch queries.
        # these will all be needed in memory anyway so this is ok.
        build_profile = self.build_profiles[build_profile_id] if build_profile_id else None
        if not multimedia_map:
            multimedia_map = self.multimedia_map_for_build(build_profile=build_profile,
                                                           remove_unused=remove_unused)
        expected_ids = [map_item.multimedia_id for map_item in multimedia_map.values()]
        raw_docs = dict((d["_id"], d) for d in iter_docs(CommCareMultimedia.get_db(), expected_ids))
        for path, map_item in multimedia_map.items():
            media_item = raw_docs.get(map_item.multimedia_id)
            if media_item:
                media_cls = CommCareMultimedia.get_doc_class(map_item.media_type)
                yield path, media_cls.wrap(media_item)
            else:
                # Re-attempt to fetch media directly from couch
                media = CommCareMultimedia.get_doc_class(map_item.media_type)
                try:
                    media = media.get(map_item.multimedia_id)
                    yield path, media
                except ResourceNotFound as e:
                    if toggles.CAUTIOUS_MULTIMEDIA.enabled(self.domain):
                        raise e

    def get_object_map(self, multimedia_map=None):
        object_map = {}
        for path, media_obj in self.get_media_objects(remove_unused=False, multimedia_map=multimedia_map):
            object_map[path] = media_obj.get_media_info(path)
        return object_map

    def get_reference_totals(self):
        """
            Returns a list of totals of each type of media in the application and total matches.
        """
        totals = []
        for mm in [CommCareMultimedia.get_doc_class(t) for t in CommCareMultimedia.get_doc_types()]:
            paths = self.get_all_paths_of_type(mm.__name__)
            matched_paths = [p for p in self.multimedia_map.keys() if p in paths]
            if len(paths) > 0:
                totals.append({
                    'media_type': mm.get_nice_name(),
                    'totals': len(paths),
                    'matched': len(matched_paths),
                    'icon_class': mm.get_icon_class(),
                    'paths': matched_paths,
                })
        return totals

    def check_media_state(self):
        has_missing_refs = False

        for media in self.all_media():
            try:
                self.multimedia_map[media.path]
            except KeyError:
                has_missing_refs = True

        return {
            "has_media": bool(self.all_media()),
            "has_form_errors": self.media_form_errors,
            "has_missing_refs": has_missing_refs,
        }

    def archive_logos(self):
        """
        Archives any uploaded logos in the application.
        """
        has_archived = False
        if LOGO_ARCHIVE_KEY not in self.archived_media:
            self.archived_media[LOGO_ARCHIVE_KEY] = {}
        for slug, logo_data in list(self.logo_refs.items()):
            self.archived_media[LOGO_ARCHIVE_KEY][slug] = logo_data
            has_archived = True
            del self.logo_refs[slug]
        return has_archived

    def restore_logos(self):
        """
        Restores any uploaded logos in the application.
        """
        has_restored = False
        if hasattr(self, 'archived_media') and LOGO_ARCHIVE_KEY in self.archived_media:
            for slug, logo_data in list(self.archived_media[LOGO_ARCHIVE_KEY].items()):
                self.logo_refs[slug] = logo_data
                has_restored = True
                del self.archived_media[LOGO_ARCHIVE_KEY][slug]
        return has_restored


from rest_framework import serializers
from django.contrib.auth.models import User
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES

#using a hyperlinked relationship between entities.
# The HyperlinkedModelSerializer has the following differences from ModelSerializer:
# It does not include the pk field by default.
# It includes a url field, using HyperlinkedIdentityField.
# Relationships use HyperlinkedRelatedField, instead of PrimaryKeyRelatedField.

# used to serialize the snippet data
class SnippetSerializer(serializers.HyperlinkedModelSerializer):

    owner = serializers.ReadOnlyField(source='owner.username')
    highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')

    class Meta:
        model = Snippet
        fields = ('url', 'highlight', 'owner', 'title', 'code', 'linenos', 'language', 'style')

# used to serialize the user data
class UserSerializer(serializers.HyperlinkedModelSerializer):
    snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True)

    class Meta:
        model = User
        fields = ('url', 'username', 'snippets')


import pygame.font

class Button:

    def __init__(self,ai_game, msg):
        """Initialize buttom attributes"""
        self.screen = ai_game.screen
        self.screen_rect = ai_game.screen.get_rect()
        self.settings = ai_game.settings

        # Set the dimensions and properties of the button
        self.width, self.height = self.settings.button_width, self.settings.button_height
        self.button_color = (0,0,0)  # Black
        self.text_color = (230,230,230) # White
        self.font = pygame.font.SysFont(None,48)

        # Build the button's rect object
        self.rect = pygame.Rect(0,0,self.width,self.height)
        self.rect.topleft = self.screen_rect.topleft

        # Prep the buttons message
        self._prep_msg(msg)

    def _prep_msg(self,msg):
        """Turn msg into a rendered image and center text on button"""
        self.msg_image = self.font.render(msg,True,self.text_color,self.button_color) # font render takes a text and turns it into
        self.msg_image_rect = self.msg_image.get_rect()                               # a surface, takes the message then a bollean
        self.msg_image_rect.center = self.rect.center                                 # representing smoothing of text, then text color
                                                                                # and text background color
    def draw_button(self):
        # Draw Blank Button and then draw message
        self.screen.fill(self.button_color,self.rect)
        self.screen.blit(self.msg_image, self.msg_image_rect)


import requests
import numpy as np

### api url call ###
def api_get(url_param, league_id):
    return requests.get(f'https://api.sleeper.app/v1/league/{league_id}/{url_param}')

### leagues ###
loeg = '720397292238073856'
sloppy_joes = '725424222041657344'

# will eventually allow user to toggle league choice
def change_league(league):
    roster_list = api_get("rosters", league).json()
    user_list = api_get("users", league).json()
    league_data = [roster_list, user_list]
    return league_data

# initializes league data
#league_data = change_league(sloppy_joes)
league_data = change_league(loeg)

### api keys ###
# user_id - user['user_id']
# id - roster['owner_id']
# teamname - user['display_name']
# wins - roster['settings']['wins']
# max points - roster['settings']['ppts']
# actual points - roster['settings']['fpts']

# gets team name
def get_name(owners, users):
    names = []
    for owner in owners:
        for user in users:
            if owner['owner_id'] == user['user_id']:
                names.append(user['display_name'])
    return names

# gets various team stats(wins, actual points, max points)
# based on input
def get_stats(res, stat):
    stat_arr = []
    for team in res:
        data = team['settings'][stat]
        stat_arr.append(data)
    return stat_arr

def get_mean(input):
    mean_input = np.mean(input)
    return mean_input

teamnames = get_name(league_data[0], league_data[1])
wins = get_stats(league_data[0], 'wins')

max_points = get_stats(league_data[0], 'ppts')
actual_points = get_stats(league_data[0], 'fpts')
# uses numpy to divide actual and
# max point arrays and get total point percentage
mp = np.array(max_points)
ap = np.array(actual_points)
percentage = ap/mp

mean_max_points = get_mean(max_points)
mean_wins = get_mean(wins)
mean_percentage = get_mean(percentage)


import DSAGraph as Graph
import re
import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
import pickle

"""
ToDo
9. test harness for all functions
10. UML diagram
11. report
"""

"""
Class: ListMenu()
Function: Allows for graphs to be made into maps for the assignment performing functions on them
Constructor Arguments: None
"""
class ListMenu():
    def __init__(self):
        self.mapGraph = Graph.DSAGraph()
        self.netGraph = nx.DiGraph()
        self.start = None
        self.end = None
        self.nMatrix = None
        self.pSec = 0
        self.pStair = False

    """
    Name: readFile
    Inputs: str fileName - Name of file to read from
    Returns: None
    Function: reads in a map file and creates a graph from it (directional graph)
    """
    def readFile(self, fileName):
        try:
            with open(fileName, "r") as file:
                for line in file:
                    #doesn't look at lines starting with comment hashtag
                    if re.search("(#.*)", line) ==  None:
                        data = line.split("|")
                        #checking direction of edge <>
                        if re.search(".*\<\>.*", data[0]):
                            #add vertex and node
                            location = data[0].split("<>")
                            loc1 = location[0]
                            loc2 = location[1]
                            d, s, b = self.getParameters(data)
                            if self.mapGraph.hasVertex(loc1) == 0:
                                self.mapGraph.addVertex(loc1, loc1)
                            if self.mapGraph.hasVertex(loc2) == 0:
                                self.mapGraph.addVertex(loc2, loc2)

                            self.mapGraph.addEdge(loc1, loc2, d, s, b)
                            self.mapGraph.addEdge(loc2, loc1, d, s, b)
                        #checking direction of edge <
                        elif re.search(".*\<[^\>].*", data[0]):
                            #add vertex and node
                            location = data[0].split("<")
                            loc1 = location[0]
                            loc2 = location[1]
                            d, s, b = self.getParameters(data)
                            if self.mapGraph.hasVertex(loc1) == 0:
                                self.mapGraph.addVertex(loc1, loc1)
                            if self.mapGraph.hasVertex(loc2) == 0:
                                self.mapGraph.addVertex(loc2, loc2)
                            self.mapGraph.addEdge(loc2, loc1, d, s, b)
                        #checking direction of edge <
                        elif re.search(".*[^\<]\>.*", data[0]):
                            #add vertex and node
                            location = data[0].split(">")
                            loc1 = location[0]
                            loc2 = location[1]
                            d, s, b = self.getParameters(data)
                            if self.mapGraph.hasVertex(loc1) == 0:
                                self.mapGraph.addVertex(loc1, loc1)
                            if self.mapGraph.hasVertex(loc2) == 0:
                                self.mapGraph.addVertex(loc2, loc2)
                            self.mapGraph.addEdge(loc1, loc2, d, s, b)
        except Exception as e:
            print(e, "Error reading file")

    """
    Name: readJourneyFile
    Inputs: str fileName - Name of file to read from
    Returns: None
    Function: reads in a journey file, sets parameters for getting from two nodes in graph
    """
    def readJourneyFile(self, fileName):
        try:
            with open(fileName, "r") as file:
                for line in file:
                    lineSplit = line.split(" ")
                    #checks for starting destination
                    if lineSplit[0] == "From":
                        #check if starting node exists in graph
                        if self.mapGraph.getLabelRef(lineSplit[1]) != None:
                            self.start = lineSplit[1]
                        else:
                            raise ValueError("Start node doesn't exist")
                    #check for destination node
                    elif lineSplit[0] == "To":
                        #check if destination node exists in graph
                        if self.mapGraph.getLabelRef(lineSplit[1]) != None:
                            self.end = lineSplit[1]
                        else:
                            raise ValueError("End node doesn't exist")
                    #check for stair needs
                    elif lineSplit[0] == "Avoid":
                        self.pStair = True
                    #check for security needs
                    elif lineSplit[0] == "Security":
                        if lineSplit[1].isdigit() == True:
                            self.pSec = int(lineSplit[1])
        except Exception as e:
            print(e)

    """
    Name: pickle
    Inputs: str fileName - Name of file to write to
    Returns: None
    Function: serialises the graph object and saves it as fileName
    """
    def pickle(self, fileName):
        try:
            with open(fileName, "wb") as dataFile:
                pickle.dump(self.mapGraph, dataFile)
        except:
            print("Error: problem pickling object")

    """
    Name: unpickle
    Inputs: str fileName - Name of file to read from
    Returns: None
    Function: reads in a serialised graph object and sets class graph to it
    """
    def unpickle(self, fileName):
        try:
            with open(fileName,"rb") as dataFile:
                newMapGraph = pickle.load(dataFile)
                #check if unpickled file is a Graph object
                if isinstance(newMapGraph, Graph.DSAGraph):
                    self.mapGraph = newMapGraph
                else:
                    raise TypeError("Unpickled object is not of Graph Type")
        except Exception as e:
            print(e)

    """
    Name: matrixToCsv
    Inputs: str fileName - name of file, 2d array matrix - adjacency matrix of graph
    Returns: None
    Function: Saves the generated adjacency matrix to a file
    """
    def matrixToCsv(self, fileName, matrix):
        nVert = self.mapGraph.getNVert()
        n = 0
        try:
            with open(fileName, "w") as file:
                #for columns
                for i in range(0, nVert, 1):
                    n = 0
                    #for row
                    for j in range(0, nVert, 1):
                        n+= 1
                        file.write(str(matrix[i][j]))
                        #check if there are values after (can't csv line on ',')
                        if n < nVert:
                            file.write(",")
                    file.write("\n")
        except Exception as e:
            print("Error writing to file " + fileName + " " + e)

    """
    Name: worldToCsv
    Inputs: str fileName - name of file to save to
    Returns: None
    Function: saves list of each vertex and their edges to a file
    """
    def worldToCsv(self, fileName):
        try:
            with open(fileName, "w") as file:
                for verts in self.mapGraph.vertList:
                    #save vertices and all adjacent edges
                    file.write(verts.toString() + "\n")
        except Exception as e:
            print("Error writing to file " + fileName + " " + e)

    """
    Name: graphToCsv
    Inputs: str fileName - name of file to save to
    Returns: None
    Function: Saves graph into a map input style file (can use readFile on it)
    """
    def graphToCsv(self, fileName):
        try:
            with open(fileName, "w") as file:
                for verts in self.mapGraph.vertList:
                    for edges in verts.links:
                        stair = ""
                        if edges.getBarriers() == True:
                            stair = "stairs"
                        #format a map file you can read from after
                        file.write(verts.getLabel() + ">" + edges.getLabel() + "|" + "D:" + str(edges.getDistance()) + "|" + "S:" + str(edges.getSecurity()) + "|" + "B:" + stair + "\n")
        except Exception as e:
            print("Error writing to file", e)

    """
    Name: getParameters
    Inputs: list data - .split() of readFile line
    Returns: list data - individual edge parameters
    Function: converts .split() readfile line data into usable edge parameters
    """
    # Note this function is used in file io
    def getParameters(self, data):
        ray = np.empty(3, dtype=object)
        b = False
        d = self.stringNumber(data[1])
        s = self.stringNumber(data[2])

        #looks for stairs string and returns number in the string
        if re.search("stairs", data[3]):
            b = True

        ray[0] = d
        ray[1] = s
        ray[2] = b

        return ray

    """
    Name: stringNumber
    Inputs: str string - string containing an integer
    Returns: int number
    Function: extracts the integer from the string ignoring all characters (using regex)
    """
    def stringNumber(self, string):
        number = None

        #finds all numbers in string
        n = re.findall("\d+", string)
        if n:
            #get last number
            number = int(n[-1])
        else:
            number = 0
        return number

    """
    Name: interactiveMenu
    Inputs: None
    Returns: None
    Function: provides a user interface to perform tasks on the graph (save/plan journey/display/node and edge functions)
    """
    def interactiveMenu(self):
        exitLoop = False
        while exitLoop == False:
            usrInput = ""
            while usrInput != "1" and usrInput != "2" and usrInput != "3" and usrInput != "4" and usrInput != "5" and usrInput != "6" and usrInput != "7" and usrInput != "8" and usrInput != "9" and usrInput != "10" and usrInput != "11":
                usrInput = input("########\n(1)Load input file\n(2)Node Operations\n(3)Edge Operations\n(4)Parameter Tweaks\n(5)Generate Matrix\n(6)Display World\n(7)Enter Journey Details\n(8)Generate Routes\n(9)Display Routes\n(10)Save\n(11)Exit\n########\n")
            if usrInput == "1":
                self.loadFile()
            elif usrInput == "2":
                self.nodeOperations()
            elif usrInput == "3":
                self.edgeOperations()
            elif usrInput == "4":
                self.parameterTweaks()
            elif usrInput == "5":
                self.nMatrix = self.mapGraph.generateMatrix()
                self.GraphMenu()
            elif usrInput == "6":
                self.displayWorldMenu()
            elif usrInput == "7":
                self.journeyDetail()
            elif usrInput == "8":
                #check if user has set a starting and ending destination
                if self.start == None and self.end == None:
                    print("Please enter the starting and ending destination first")
                else:
                    print("Generating journey...")
                    self.pathFind()
                    print(self.mapGraph.fPath.count)
            elif usrInput == "9":
                self.displayRoute()
            elif usrInput == "10":
                self.SaveFileMenu()
            elif usrInput == "11":
                exitLoop = True

    """
    Name: displayWorldMenu
    Inputs: None
    Returns: None
    Function: provides a user interface to allow user to generate and display the graph in a readable format
    """
    def displayWorldMenu(self):
        exitLoop = False
        while exitLoop == False:
            usrInput = ""
            while usrInput != "1" and usrInput != "2" and usrInput != "3":
                usrInput = input("########\nDisplay World\n(1)Display Graph\n(2)Display Nodes and their Edges\n(3)Exit\n########\n")
            if usrInput == "1":
                self.generateNetwork()
                self.displayNetwork()
            elif usrInput == "2":
                #display vertices and their edges
                for verts in self.mapGraph.vertList:
                    print(verts.toString())
                inputChoice = ""
                while inputChoice != "y" and inputChoice != "n":
                    inputChoice = input("Save to file (y/n)")
                if inputChoice == "y":
                    fileName = input("Enter a filename: ")
                    self.worldToCsv(fileName)
            elif usrInput == "3":
                exitLoop = True

    """
    Name: nodeOperations
    Inputs: None
    Returns: None
    Function: provides a user interface to allow user to perform operations/edit vertices/nodes of the graph
    """
    def nodeOperations(self):
        exitLoop = False
        while exitLoop == False:
            usrInput = ""
            while usrInput != "1" and usrInput != "2" and usrInput != "3" and usrInput != "4" and usrInput != "5":
                usrInput = input("########\nNode Operations\n(1)find\n(2)insert\n(3)delete\n(4)update\n(5)Exit\n########\n")
            if usrInput == "1":
                print(self.mapGraph.listVerts())
                choice = input("Name of vertex to find: ")
                print(self.mapGraph.getVertex(choice))
            elif usrInput == "2":
                label = input("Name of vertex: ")
                self.mapGraph.addVertex(label, label)
            elif usrInput == "3":
                print(self.mapGraph.listVerts())
                choice = input("Name of vertex to delete: ")
                self.mapGraph.deleteVert(choice)
                self.mapGraph.deleteEdges(choice)
            elif usrInput == "4":
                print(self.mapGraph.listVerts())
                choice = input("Which vertex name do you want to update: ")
                newLabel = input("What do you want the new name to be: ")
                if self.mapGraph.getVertex(choice) == ("Vertex not in graph"):
                    print("Vertex not in graph")
                else:
                    self.mapGraph.updateVertName(choice, newLabel)
            elif usrInput == "5":
                exitLoop = True

    """
    Name: edgeOperations
    Inputs: None
    Returns: None
    Function: provides a user interface to allow user to perform operations on edge in the graph
    """
    def edgeOperations(self):
        exitLoop = False
        while exitLoop == False:
            usrInput = ""
            while usrInput != "1" and usrInput != "2" and usrInput != "3" and usrInput != "4" and usrInput != "5":
                usrInput = input("########\nEdge Operations:\n(1)find\n(2)insert\n(3)delete\n(4)update\n(5)Exit\n########\n")
            if usrInput == "1":
                #give user list of start and vertices to choose from
                print(self.mapGraph.listVerts())
                start = input("Starting vertex: ")
                end = input("Destination vertex: ")
                n = self.mapGraph.getNAdjacentEdge(start, end)
                #print all edges in direction
                print("There are: ", n , " edges")
                try:
                    for i in range(1, n+1, 1):
                        print(self.mapGraph.getEdge(start, end, i))
                except Exception as e:
                    print(e)
            elif usrInput == "2":
                security = ""
                distance = ""
                stairs = False
                print(self.mapGraph.listVerts())
                #choose direction of edge
                start = input("Starting vertex: ")
                end = input("Destination vertex: ")
                #choose security
                while not security.isdigit():
                    security = input("N of security: ")
                if int(security) <= 0:
                    security = 0
                #choose distance
                while not distance.isdigit():
                    distance = input("distance: ")
                #set stairs
                while stairs != "y" and stairs != "n":
                    stairs = input("Any stairs? (y/n): ")
                if stairs == "y":
                    stairs = True
                else:
                    stairs = False
                try:
                    self.mapGraph.addEdge(start, end, distance, int(security), stairs)
                except Exception as e:
                    print(e)

            elif usrInput == "3":
                print(self.mapGraph.listVerts())
                start = input("Starting vertex: ")
                end = input("Destination vertex: ")
                #choose direction of edge
                n = self.mapGraph.getNAdjacentEdge(start, end)
                #print number of edges
                print("There are: ", n , " edges")
                try:
                    #print all edges
                    for i in range(1, n+1, 1):
                        print(self.mapGraph.getEdge(start, end, i))
                except Exception as e:
                    print(e)
                #if more than one edge, give user option to select edge for deletion
                if n>=2:
                    inpChoice = 0
                    while inpChoice > n or inpChoice <= 0:
                        inpChoice = int(input("Which edge would you like to delete (1-"+str(n)+"): "))
                    self.mapGraph.deleteEdge(start, end, int(inpChoice))
                else:
                    self.mapGraph.deleteEdge(start, end, 1)

            elif usrInput == "4":
                self.edgeUpdate()
            elif usrInput == "5":
                exitLoop = True

    """
    Name: parameterTweaks
    Inputs: None
    Returns: None
    Function: provides user interface to user to adjust parameters in the journey
    """
    def parameterTweaks(self):
        exitLoop = False
        while exitLoop == False:
            usrInput = ""
            while usrInput != "1" and usrInput != "2" and usrInput != "3":
                usrInput = input("########\nParameter Tweaks:\n(1)Change Stair Requirements\n(2)Change Security Requirements\n(3)Exit\n########\n")
            if usrInput == "1":
                stairs = ""
                #input needs to be y/n, doesn't compare to set value when changing
                while stairs != "y" and stairs != "n":
                    stairs = input("Avoid stairs (y/n): ")
                #convert to boolean
                if stairs == "y":
                    stairs = True
                else:
                    stairs = False
                self.pStair = stairs
            elif usrInput == "2":
                security = ""
                #input needs to be int
                while not security.isdigit():
                    security = input("N of security: ")
                security = int(security)
                #security > 0 no upper bounds
                if security <= 0:
                    security = 0
                self.pSec = security
            elif usrInput == "3":
                exitLoop = True

    """
    Name: journeyDetail
    Inputs: None
    Returns: None
    Function: provides a user interface for user to set start and end desination of journey
    """
    def journeyDetail(self):
        exitLoop = False
        while exitLoop == False:
            usrInput = ""
            while usrInput != "1" and usrInput != "2" and usrInput != "3":
                usrInput = input("########\nJourney Details:\n(1)Starting Node\n(2)Destination Node\n(3)Exit\n########\n")
            if usrInput == "1":
                #check if there is more than 1 vert (for start and end destination)
                if self.mapGraph.getNVert() >= 1:
                    #user must choose a vert in graph
                    inChoice = ""
                    while self.mapGraph.getVertex(inChoice) == "No such vertex":
                        print(self.mapGraph.listVerts())
                        inChoice = input("Enter starting Node: ")
                    self.start = inChoice
                else:
                    print("No nodes")
            elif usrInput == "2":
                #check if there is more than 1 vert (for start and end destination)
                if self.mapGraph.getNVert() >= 1:
                    inChoice = ""
                    #user must choose a vert in graph
                    while self.mapGraph.getVertex(inChoice) == "No such vertex":
                        print(self.mapGraph.listVerts())
                        inChoice = input("Enter Ending Node: ")
                    self.end = inChoice
                else:
                    print("No Nodes")
            elif usrInput == "3":
                exitLoop = True

    """
    Name: GraphMenu
    Inputs: None
    Returns: None
    Function: provides a user interface for a user to generate/display/save an adjacency matrix of the graph
    """
    def GraphMenu(self):
        exitLoop = False
        while exitLoop == False:
            usrInput = ""
            while usrInput != "1" and usrInput != "2" and usrInput != "3":
                usrInput = input("########\nJourney Details:\n(1)Display Graph\n(2)Save Graph\n(3)Exit\n########\n")
            if usrInput == "1":
                self.mapGraph.displayMatrix(self.nMatrix)
            elif usrInput == "2":
                fileName = input("Enter name of map file: ")
                self.matrixToCsv(fileName, self.nMatrix)
            elif usrInput == "3":
                exitLoop = True

    """
    Name: saveFileMenu
    Inputs: None
    Returns: None
    Function: provides a user interface to save the graph object to a csv or serialised file
    """
    def SaveFileMenu(self):
        exitLoop = False
        while exitLoop == False:
            usrInput = ""
            while usrInput != "1" and usrInput != "2" and usrInput != "3":
                usrInput = input("########\nSave File:\n(1)To File\n(2)Pickle\n(3)Exit\n########\n")
            if usrInput == "1":
                fileName = input("Enter name of map file: ")
                self.graphToCsv(fileName)
            elif usrInput == "2":
                fileName = input("Enter name of map file: ")
                self.pickle(fileName)
            elif usrInput == "3":
                exitLoop = True

    """
    Name: edgeUpdate
    Inputs: None
    Returns: None
    Function: provides a user interface for the user to change parameters in edges
    """
    def edgeUpdate(self):
        exitLoop = False
        while exitLoop == False:
            usrInput = ""
            while usrInput != "1" and usrInput != "2" and usrInput != "3" and usrInput != "4":
                usrInput = input("########\nEdge Value Update:\n(1)Stairs\n(2)Security\n(3)Distance\n(4)Exit\n########\n")
            if usrInput == "1":
                newStair = ""
                #choosing edge from verts
                print(self.mapGraph.listVerts())
                start = input("Starting vertex: ")
                end = input("Destination vertex: ")
                #can be more than one edge
                n = self.mapGraph.getNAdjacentEdge(start, end)
                print("There are: ", n , " edges")
                try:
                    for i in range(1, n+1, 1):
                        print(self.mapGraph.getEdge(start, end, i))
                except Exception as e:
                    print(e)
                #choose edge to update
                if n>=2:
                    inpChoice = 0
                    while inpChoice > n or inpChoice <= 0:
                        inpChoice = int(input("Which edge would you like to update(1-"+str(n)+"): "))
                else:
                    inpChoice = 1

                try:
                    #update edge stair parameter
                    print(self.mapGraph.getEdge(start, end, inpChoice))
                    while(newStair != "y" and newStair != "n"):
                        newStair = input("Is there stairs in edge? (y/n): ")
                    if newStair == "y":
                        newStair = True
                    else:
                        newStair = False
                    self.mapGraph.updateEdgeBarriers(start, end, newStair, inpChoice)
                except Exception as e:
                    print(e)
            elif usrInput == "2":
                newSecurity = ""
                #choosing edge from verts
                print(self.mapGraph.listVerts())
                start = input("Starting vertex: ")
                end = input("Destination vertex: ")
                #can be more than one edge
                n = self.mapGraph.getNAdjacentEdge(start, end)
                print("There are: ", n , " edges")
                try:
                    for i in range(1, n+1, 1):
                        print(self.mapGraph.getEdge(start, end, i))
                except Exception as e:
                    print(e)
                #choose edge
                if n>=2:
                    inpChoice = 0
                    while inpChoice > n or inpChoice <= 0:
                        inpChoice = int(input("Which edge would you like to update (1-"+str(n)+"): "))
                else:
                    inpChoice = 1

                try:
                    #update security parameter
                    print(self.mapGraph.getEdge(start, end, inpChoice))
                    while(not newSecurity.isnumeric()):
                        newSecurity = input("How much security: (number)")

                    self.mapGraph.updateEdgeSecurity(start, end, newSecurity, inpChoice)
                except Exception as e:
                    print(e)
            elif usrInput == "3":
                newDistance = ""
                #choosing edge from verts
                print(self.mapGraph.listVerts())
                start = input("Starting vertex: ")
                end = input("Destination vertex: ")
                #can be more than one edge in same direction
                n = self.mapGraph.getNAdjacentEdge(start, end)
                print("There are: ", n , " edges")
                try:
                    for i in range(1, n+1, 1):
                        print(self.mapGraph.getEdge(start, end, i))
                except Exception as e:
                    print(e)
                #choosing edge
                if n>=2:
                    inpChoice = 0
                    while inpChoice > n or inpChoice <= 0:
                        inpChoice = int(input("Which edge would you like to update (1-"+str(n)+"): "))
                else:
                    inpChoice = 1

                try:
                    #updating distance parameter
                    print(self.mapGraph.getEdge(start, end, inpChoice))
                    while(not newDistance.isnumeric()):
                        newDistance = input("How long is distance: (number)")
                    self.mapGraph.updateEdgeDistance(start, end, newDistance, inpChoice)
                except Exception as e:
                    print(e)
            elif usrInput == "4":
                exitLoop = True

    """
    Name: generateNetwork
    Inputs: none
    Returns: DiGraph newNetwork - networkx graph of graph object
    Function: converts map graph into a displayable graph
    """
    def generateNetwork(self):
        #create new newtorkx graph
        self.netGraph = newNetwork = nx.DiGraph()

        #iterate through all vertices adding all edges
        for nodes in self.mapGraph.vertList:
            for edges in nodes.links:
                n = nodes.getLabel()
                e = edges.getLabel()
                newNetwork.add_edge(n, e, nEdges=self.mapGraph.getNAdjacentEdge(n, e))
        return newNetwork

    """
    Name: displayNetwork
    Inputs: None
    Returns: None
    Function: displays generated networkx graph to user (has functionality to save)
    """
    def displayNetwork(self):
        #generate specific layout
        pos = nx.spring_layout(self.netGraph)
        #axis to draw on
        ax = plt.gca()
        #draw graph on axis, with labels
        nx.draw_networkx(self.netGraph, pos, ax=ax, with_labels=True, font_color="#4B6587", font_size=8, node_size=1500, node_color="#F0E5CF", connectionstyle = ("Arc3, rad=0.2"))
        plt.show()

    """
    Name: displayRoute
    Inputs: None
    Returns: None
    Function: provides a user interface to allow user to display and save generated journey routes
    """
    def displayRoute(self):
        exitLoop = False
        while exitLoop == False:
            usrInput = ""
            while usrInput != "1" and usrInput != "2" and usrInput != "3" and usrInput != "4":
                usrInput = input("########\nDisaplay Route:\n(1)Display Routes\n(2)Display Ranked Routes\n(3)Save\n(4)Exit\n########\n")
            if usrInput == "1":
                self.mapGraph.printDFSFind()
            elif usrInput == "2":
               self.displayRankedRoutes()
            elif usrInput == "3":
               fileName = input("Enter a filename: ")
               self.saveRankedRoutes(fileName)
            elif usrInput == "4":
                exitLoop = True

    """
    Name: loadFile
    Inputs: None
    Returns: None
    Function: provides a user interface to allow user to choose between reading in a serialised file and a csv file
    """
    def loadFile(self):
        exitLoop = False
        while exitLoop == False:
            usrInput = ""
            while usrInput != "1" and usrInput != "2" and usrInput != "3":
                usrInput = input("########\nLoad File:\n(1)Read File\n(2)Unpickle Object\n(3)Exit\n########\n")
            if usrInput == "1":
                fileName = input("Enter name of map file: ")
                self.readFile(fileName)
            elif usrInput == "2":
                fileName = input("Enter name of file: ")
                #checking if right instance done in unpickle method
                self.unpickle(fileName)
            elif usrInput == "3":
                exitLoop = True

    """
    Name: pathFind
    Inputs: None
    Returns: None
    Function: Accesses graph object pathfinding function to generate path then saves path to class variables
    """
    def pathFind(self):
        #search based on start and end nodes and preset/set parameters
        self.mapGraph.dfsPathFind(self.start, self.end, self.pStair, self.pSec)

    """
    Name: displayRankedRoutes
    Inputs: None
    Returns: None
    Function: displays generated routes in order of distance needed to travel from start to finish
    """
    def displayRankedRoutes(self):
        sortedRay = self.sortRoutes(self.mapGraph.getDFSRoutes())
        for rows in sortedRay:
            print("Distance: " + str(rows[1]), end = " Journey: ")
            count = 0
            for values in rows[0]:
                #print each edge in journey one after the other
                if count == 0 or isinstance(values, Graph.DSAVertex):
                    print(values.getLabel(), end = "; ")
                else:
                    print(values.getLabel() + "  D:" + str(values.getDistance()) + " Sec:"+str(values.getSecurity()) + " Stairs:" + str(values.getBarriers()) , end= "; ")
                count += 1
            print("") #newline for next longer journey

    """
    Name: saveRankedRoutes
    Inputs: str fileName - name of file to save to
    Returns: None
    Function: saves generated routes in order of distance to a file
    """
    def saveRankedRoutes(self, fileName):
        #sort routes before save
        sortedRay = self.sortRoutes(self.mapGraph.getDFSRoutes())
        try:
            with open(fileName, "w") as dataFile:
                for rows in sortedRay:
                    dataFile.write("Distance: " + str(rows[1]) +  " Journey: ")
                    count = 0
                    for values in rows[0]:
                        if count == 0 or isinstance(values, Graph.DSAVertex):
                            #start of journey list is vertice
                            dataFile.write(values.getLabel() + " ")
                        else:
                            #other nodes are edges
                            dataFile.write(values.getLabel() + "  D:" + str(values.getDistance()) + " Sec:"+str(values.getSecurity()) + " Stairs:" + str(values.getBarriers()) + "; ")
                        count += 1
                    dataFile.write("\n") #next slower journey
        except Exception as e:
            print("Error reading file " + e)

    """
    Name: sortRoutes
    Inputs: linkedList routeList - linked list containing all generated routes (each linked list index is a route)
    Returns: 2d array sortedRay - 2d array containing routes and their distances (sorted smallest to largest)
    Function:
    """
    def sortRoutes(self, routeList):
        nVals = routeList.getCount()
        sortedRay = np.empty([nVals, 2], dtype=object)

        count = 0
        #for each route
        for values in routeList:
            #save route to array
            sortedRay[count][0] = values
            dist = 0
            #sum of distances
            for i in range(0, len(values)):
                if i != 0 and isinstance(values[i], Graph.DSAEdge):
                    dist += values[i].getDistance()
            sortedRay[count][1] = dist
            count += 1

        #sort array on distances
        sortedRay = self.insertionSort(sortedRay, nVals)
        return sortedRay

    """
    Name: insertionSort
    Inputs: 2d array routeList - linked list of all routes and their distances, int length - number of routes generated
    Returns: 2d array routeList - sorted array based on route distances
    Function: Sorts a 2d array of routes from smallest to largest based on the distance of journey
    """
    def insertionSort(self, routeList, length):
        #for each index
        for n in range(1,length):
            i = n
            #check if i is smaller than n index and swap if true
            while i>0 and routeList[i-1][1] > routeList[i][1]:
                temp = routeList[i].copy()
                routeList[i] = routeList[i-1]
                routeList[i-1] = temp
                i -= 1
        return routeList

    #Insertion sort code taken and adapted from my prac 1 submission

#test function for interactive menu
if __name__ == "__main__":
    test = ListMenu()
    test.interactiveMenu()


from mimetypes import init
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
import os

db = SQLAlchemy()
BASEDIR = os.path.dirname(os.path.realpath(__file__))

def create_app():
    app = Flask(__name__)
    app.config['SECRET_KEY'] = 'thisisaverysecretkey'
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///movies.db'

    db.init_app(app)

    login_manager = LoginManager()
    login_manager.init_app(app)
    login_manager.login_view = 'auth.login'

    from .models import Users

    init_db(app)

    @login_manager.user_loader
    def load_user(id):
        return Users.query.get(id)

    from .views import views as views_blueprint
    app.register_blueprint(views_blueprint)

    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint)

    # https://stackoverflow.com/questions/6036082/call-a-python-function-from-jinja2
    from .request import get_movie_genres
    app.jinja_env.globals.update(get_movie_genres=get_movie_genres)

    return app

def init_db(app):
    if not os.path.lexists(os.path.join(BASEDIR, '\movies.db')):
        db.create_all(app=app)


#!/usr/bin/env python
#-*- coding:utf-8 _*-
"""
@author: Selonsy
@file: common.py
@time: 2019-01-09

公用的一些方法

"""

import os
import requests
import click
from utils import glovar
from utils import echo
from utils.customlog import CustomLog

logger = CustomLog(__name__).getLogger()

def get_unique_outfile(outdir, filename):
    ''' 判断文件是否已存在，并返回一个唯一名称 '''
    outfile = os.path.abspath(os.path.join(outdir, filename))
    if os.path.exists(outfile):
        name, ext = filename.rsplit('.', 1)
        names = [x for x in os.listdir(outdir) if x.startswith(name)]
        names = [x.rsplit('.', 1)[0] for x in names]
        suffixes = [x.replace(name, '') for x in names]
        # filter suffixes that match ' (x)' pattern
        suffixes = [x[2:-1] for x in suffixes
                       if x.startswith(' (') and x.endswith(')')]
        indexes  = [int(x) for x in suffixes
                       if set(x) <= set('0123456789')]
        idx = 1
        if indexes:
            idx += sorted(indexes)[-1]
        outfile = os.path.abspath(os.path.join(outdir, '%s (%d).%s' % (name, idx, ext)))
    return outfile

def music_download(music):
    ''' 下载音乐保存到本地 '''
    echo.info(music)
    outfile = get_unique_outfile(glovar.get_option('outdir'), music['name'])
    try:
        r = requests.get(music['url'], stream=True,
                         headers=glovar.WGET_HEADERS,
                         proxies=glovar.get_option('proxies'))
        total_size = int(r.headers['content-length'])
        with click.progressbar(length=total_size, label='Downloading...') as bar:
            with open(outfile, 'wb') as f:
                for chunk in r.iter_content(chunk_size=1024):
                    if chunk:
                        f.write(chunk)
                        bar.update(len(chunk))
        print('已保存到：%s\n' % outfile)
    except Exception as e:
        print('')
        logger.error('下载音乐失败：')
        logger.error('URL：%s' % music['url'])
        logger.error('位置：%s\n' % outfile )
        if glovar.get_option('verbose'):
            logger.error(e)

def url_available(url) -> bool:
    s = requests.Session()
    s.headers.update(glovar.FAKE_HEADERS)
    r = s.head(url)
    return r.status_code == requests.codes.ok

def content_length(url) -> int:
    s = requests.Session()
    s.headers.update(glovar.FAKE_HEADERS)
    try:
        r = s.head(url)
        if r.status_code == requests.codes.ok:
            return int(r.headers.get('Content-Length', 0))
    except:
        pass
    return 0

def music_list_merge(music_list) -> list:
    ''' 搜索结果合并 '''
    # 先排序
    music_list.sort(key=lambda music: (music['singer'], music['title'], music['size']), reverse=True)
    result_list = []
    for i in range(len(music_list)):
        # 如果名称、歌手都一致的话就去重，保留最大的文件
        if i != 0 \
            and music_list[i]['size'] <= music_list[i-1]['size'] \
            and music_list[i]['title'] == music_list[i-1]['title'] \
            and music_list[i]['singer'] == music_list[i-1]['singer']:
            continue
        result_list.append(music_list[i])

    return result_list


from sys import byteorder
from array import array
from struct import pack

import pyaudio
import wave
import datetime
import threading
import random

# thresholds defined as volume to consider between words.
different_thresholds = [1200, 1500, 1700, 1900, 2200]
volume_threshold = 1500
CHUNK_SIZE = 1024
FORMAT = pyaudio.paInt16
RATE = 44100
SILENCE_LENGTH = 1 # number of consecutive 'silent' audio chunks before we decide to cut the word.
WAIT_FOR_SND = 3 # number of samples we should 'keep' after the silence is detected (keeps a dying off voice)

def is_silent(snd_data):
    "Returns 'True' if below the 'silent' threshold"
    return max(snd_data) < volume_threshold

def normalize(snd_data):
    "Average the volume out"
    MAXIMUM = 16384
    times = float(MAXIMUM)/max(abs(i) for i in snd_data)

    r = array('h')
    for i in snd_data:
        r.append(int(i*times))
    return r

def trim(snd_data):
    "Trim the blank spots at the start and end"
    def _trim(snd_data):
        snd_started = False
        r = array('h')

        for i in snd_data:
            if not snd_started and abs(i)>volume_threshold:
                snd_started = True
                r.append(i)

            elif snd_started:
                r.append(i)
        return r

    # Trim to the left
    snd_data = _trim(snd_data)

    snd_data.reverse()
    snd_data = _trim(snd_data)
    snd_data.reverse()
    return snd_data

def add_silence(snd_data, seconds):
    "Add silence to the start and end of 'snd_data' of length 'seconds' (float)"
    r = array('h', [0 for i in range(int(seconds*RATE))])
    r.extend(snd_data)
    r.extend([0 for i in range(int(seconds*RATE))])
    return r

def record(stream):
    """
    Record a word or words from the microphone and
    return the data as an array of signed shorts.

    Normalizes the audio, trims silence from the
    start and end, and pads with 0.5 seconds of
    blank sound to make sure VLC et al can play
    it without getting chopped off.
    """

    num_silent = 0
    snd_started = False

    r = array('h')

    wait_for_snd = 0
    while 1:
        # little endian, signed short
        snd_data = array('h', stream.read(CHUNK_SIZE))
        if byteorder == 'big':
            snd_data.byteswap()
        r.extend(snd_data)

        silent = is_silent(snd_data)
        if silent and wait_for_snd:
            wait_for_snd -= 1
        if silent and snd_started:
            num_silent += 1
        elif not silent and not snd_started:
            snd_started = True
            wait_for_snd = WAIT_FOR_SND

        if snd_started and num_silent > SILENCE_LENGTH:
            break

    sample_width = p.get_sample_size(FORMAT)

    # format the data to be normalized and trimmed.
    r = normalize(r)
    r = trim(r)
    # randomly adds silence and silence thresholds on 20 percent of the data.
    # this helps varying amounts of silence to sounds and still classifying them correctly
    if random.random() < 0.2:
        r = add_silence(r, 0.2)
    volume_threshold = random.choice(different_thresholds)
    return sample_width, r

def record_to_file(path, stream):
    "Records from the microphone and outputs the resulting data to 'path'"
    sample_width, data = record(stream)
    data = pack('<' + ('h'*len(data)), *data)

    filename = str(datetime.datetime.now()).replace('.', '').replace(':', '_') + ".wav"
    wf = wave.open(path + filename, 'wb')
    wf.setnchannels(1)
    wf.setsampwidth(sample_width)
    wf.setframerate(RATE)
    wf.writeframes(data)
    wf.close()

if __name__ == '__main__':
    """
    Main program asks you to speak rich and not rich, and places words it detects into files.
    """
    p = pyaudio.PyAudio()
    stream = p.open(format=FORMAT, channels=1, rate=RATE,
        input=True, output=True,
        frames_per_buffer=CHUNK_SIZE)

    finished_recording = False
    def check_key_press():
        global finished_recording
        finished_recording = False
        input('')
        finished_recording = True

    print("please say RICH words into the microphone. press enter when finished.")
    threading.Thread(target = check_key_press).start()
    richs_gained = 0
    while not finished_recording:
        richs_gained += 1
        print('richs gained:', richs_gained, end='\r')
        record_to_file('rich/', stream)
    print("done - results written to 'rich' folder")

    print("please say NON RICH words into the microphone. press enter when finished.")
    threading.Thread(target = check_key_press).start()
    not_richs_gained = 0
    while not finished_recording:
        not_richs_gained += 1
        print('not riches gained:', not_richs_gained, end='\r')
        record_to_file('notrich/', stream)

    stream.stop_stream()
    stream.close()
    p.terminate()

    print("done - results written to 'notrich' folder")
    print('Goodbye.')


from hashlib import algorithms_available
from random import randint
from statistics import mean

from guessing_algorithms import GuessingAlgorithm, GuessingBottomUp, GuessingMiddleUpOrDown, GuessingRandom, GuessingUpBottom, ValueNotInRangeError

def give_feedback(guess: int, correct_value: int):
    if correct_value > guess:
        return 'low'
    elif correct_value < guess:
        return 'high'
    else:
        return 'hit'

def run_non_interactive_prompt(algorithm: GuessingAlgorithm, min_int: int, max_int: int, correct_value: int) -> int:
    guesser = algorithm(min_int, max_int)
    counter = 0
    while True:
        counter += 1
        # First, get next guess value
        guess = guesser.guess()
        result = give_feedback(guess, correct_value)
        if result == "hit":
            break
        # Try to get feedback (if GuessingMiddleUpOrDown algorithm)
        try:
            guesser.get_feedback(result, guess)
        except:
            continue
    return counter

def main():
    algorithms = [GuessingBottomUp, GuessingMiddleUpOrDown,
                  GuessingRandom, GuessingUpBottom]
    min_value, max_value = 1, 10000
    actual_values = [randint(min_value, max_value) for _ in range(500)]
    for algorithm in algorithms:
        print(algorithm.__name__)
        n_tries = list(map(lambda x: run_non_interactive_prompt(
            algorithm, min_value, max_value, x), actual_values))
        print(mean(n_tries))

if __name__ == "__main__":
    main()


from math import pi
from typing import Union, Optional

import numpy as np
import pandas as pd
from pandas import DatetimeIndex
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.utils.validation import check_is_fitted

from gtime.base import FeatureMixin, add_class_name

__all__ = ["PeriodicSeasonal", "Constant"]

# TODO: add something like 'make_periodic_feature' or 'make_sinusoid'
class PeriodicSeasonal(BaseEstimator, TransformerMixin, FeatureMixin):
    """Create a sinusoid from a given date and with a given period and amplitude.

    Parameters
    ----------
    period : Union[pd.Timedelta, str], optional, default: ``'365 days'``
        The period of the generated time series.

    amplitude : float, optional, default: ``0.5``
        The amplitude of the time series.

    start_date : Union[pd.Timestamp, str], optional, default: ``None``
        The date from which to start generating the feature. This is used only if X is
        not passed in the ``transform`` method, otherwise the start date is inferred
        from it.

    length : int, optional, default: ``50``
        The length of the sinusoid. This is used only if X is not passed in the
        ``transform`` method, otherwise the length is inferred from it.

    index_period : Union[DatetimeIndex, int], optional, default: ``None``
        The period of the index of the output ``DataFrame``. This is used only if X is
        not passed in the ``transform`` method, otherwise the index period is taken
        from it.

    Examples
    --------
    >>> import pandas as pd
    >>> from gtime.feature_generation import PeriodicSeasonal
    >>> X = pd.DataFrame(range(0, 10), index=pd.date_range(start='2019-04-18',  end='2019-04-27', freq='d'))
    >>> periodic = PeriodicSeasonal()
    >>> periodic.fit_transform(X)
                0__PeriodicSeasonal
    2019-04-18             0.000000
    2019-04-19             0.008607
    2019-04-20             0.017211
    2019-04-21             0.025810
    2019-04-22             0.034401
    2019-04-23             0.042982
    2019-04-24             0.051551
    2019-04-25             0.060104
    2019-04-26             0.068639
    2019-04-27             0.077154

    """

    def __init__(
        self,
        period: Union[pd.Timedelta, str] = "365 days",
        amplitude: float = 0.5,
        start_date: Optional[Union[pd.Timestamp, str]] = None,
        length: Optional[int] = 50,
        index_period: Optional[Union[DatetimeIndex, int]] = None,
    ):
        self.start_date = start_date
        self.period = period
        self.amplitude = amplitude
        self.length = length
        self.index_period = index_period

    def fit(self, X: pd.DataFrame, y=None) -> "PeriodicSeasonal":
        """Fit the estimator.

        Parameters
        ----------
        X : pd.DataFrame, shape (n_samples, n_features), required
            Input data.

        y : None
            There is no need of a target in a transformer, yet the pipeline API
            requires this parameter.

        Returns
        -------
        self : PeriodicSeasonal
            Returns self.

        """
        self.columns_ = X.columns.values
        return self

    @add_class_name
    def transform(self, time_series: Optional[pd.DataFrame] = None) -> pd.DataFrame:
        """Generate a sinusoid, with the given ``period``, ``amplitude`` and ``length``,
        starting from the selected ``start_date``. If ``time_series`` is not ``None``,
        the ``start_date`` is replaced by the start date of the time series and the
        output sinusoid will have the same index as ``time_series``.

        Parameters
        ----------
        time_series : pd.DataFrame, shape (n_samples, 1), optional, default: ``None``
            The input DataFrame, If passed, the output DataFrame is going to have the
            same index as ``time_series``. If is not passed, then the ``start_date`` and
            ``index_period`` must have been passed in the constructor when the object
            was instantiated.

        Returns
        -------
        periodic_feature : pd.DataFrame, shape (n_samples, 1)
            The DataFrame containing the generated period feature.

        Raises
        ------
        ValueError
            Raised if ``time_series`` is not passed and the ``start_date`` or the
            ``index_period`` are not present.

        """
        self._validate_input(time_series)
        self._transform_inputs(time_series)
        datetime_index = self._get_time_index(time_series)

        periodic_feature_values = self._compute_periodic_feature(datetime_index)
        if time_series is not None:
            periodic_feature = pd.DataFrame(
                index=time_series.index, data=periodic_feature_values
            )

        else:
            periodic_feature = pd.DataFrame(data=periodic_feature_values[: self.length])

        return periodic_feature

    def _validate_input(self, X: pd.DataFrame) -> None:
        if X is None:
            if self.start_date is None:
                raise ValueError(
                    f"If X is not provided, the 'start_date' variable "
                    f"must be provided, but instead was "
                    f"{self.start_date}."
                )
            if self.index_period is None:
                raise ValueError(
                    f"If X is not provided, the 'index_period' variable "
                    f"must be provided, but instead was "
                    f"{self.index_period}."
                )

    def _transform_inputs(self, X: pd.DataFrame) -> None:
        if isinstance(self.period, str):
            self.period = pd.to_timedelta(self.period)

        if X is not None:
            self.start_date = X.index.values[0]
        else:
            if isinstance(self.start_date, str):
                self.start_date = pd.to_datetime(self.start_date)

    def _get_time_index(self, X: pd.DataFrame) -> pd.DatetimeIndex:
        if X is not None:
            datetime_index = X.index
        else:
            if isinstance(self.index_period, int):
                datetime_index = pd.date_range(
                    start=self.start_date, periods=self.index_period
                )
            else:
                datetime_index = self.index_period

        self._check_sampling_frequency(datetime_index)

        return datetime_index

    def _check_sampling_frequency(self, datetime_index: pd.DatetimeIndex) -> None:
        if isinstance(datetime_index, pd.PeriodIndex):
            datetime_index = datetime_index.to_timestamp()
        sampling_frequency = pd.Timedelta(datetime_index[1] - datetime_index[0])

        if self.period < 2 * sampling_frequency:
            raise ValueError(
                f"Sampling frequency must be at least two times"
                f"the period to obtain meaningful results. "
                f"Sampling frequency = {sampling_frequency},"
                f"period = {self.period}."
            )

    def _compute_periodic_feature(self, datetime_index: pd.DatetimeIndex):
        return (
            np.sin(2 * pi * (datetime_index - self.start_date) / self.period)
        ) * self.amplitude

class Constant(BaseEstimator, TransformerMixin, FeatureMixin):
    """Generate a ``pd.DataFrame`` with one column, of the same length as the input
    ``X`` and containing the value ``constant`` across the whole column.

    Parameters
    ----------
    constant : int, optional, default: ``2``
        The value to use to generate the constant column of the ``pd.DataFrame``.

    length : int, optional, default: ``50``
        The length of the DataFrame to generate. This is used only if X is not passed in
        the ``transform`` method, otherwise the length is inferred from it.

    Examples
    --------
    >>> import pandas as pd
    >>> from gtime.feature_generation import Constant
    >>> X = pd.DataFrame(range(0, 5), index=pd.date_range(start='2019-04-18',  end='2019-04-22', freq='d'))
    >>> constant = Constant(constant=3)
    >>> constant.fit_transform(X)
                0__Constant
    2019-04-18          3.0
    2019-04-19          3.0
    2019-04-20          3.0
    2019-04-21          3.0
    2019-04-22          3.0

    """

    def __init__(self, constant: int = 0, length: int = None):
        super().__init__()
        self.length = length
        self.constant = constant

    def get_feature_names(self):
        """Return feature names for output features.

        Returns
        -------
        output_feature_names : ndarray, shape (n_output_features,)
            Array of feature names.

        """

        return [self.__class__.__name__]

    def fit(self, X: pd.DataFrame, y=None) -> "Constant":
        """Fit the estimator.

        Parameters
        ----------
        X : pd.DataFrame, shape (n_samples, n_features)
            Input data.

        y : None
            There is no need of a target in a transformer, yet the pipeline API
            requires this parameter.

        Returns
        -------
        self : Constant
            Returns self.

        """
        self.columns_ = X.columns.values
        self.rows_ = X.shape[0]
        self.length_ = (
            np.min([self.length, self.rows_]) if self.length is not None else self.rows_
        )
        return self

    @add_class_name
    def transform(self, time_series: Optional[pd.DataFrame] = None) -> pd.DataFrame:
        """Generate a ``pd.DataFrame`` with one column with the same length as
        ``time_series`` and with the same index, containing a value equal to
        ``constant``.

        Parameters
        ----------
        time_series : pd.DataFrame, shape (n_samples, 1), optional, default: ``None``
            The input DataFrame. If passed, the output DataFrame is going to have the
            same index as ``time_series``.

        Returns
        -------
        constant_series_renamed : pd.DataFrame, shape (length, 1)
            A constant series, with the same length of ``X`` and with the same index.

        """
        check_is_fitted(self)

        constants = np.full(self.rows_, np.nan)
        constants[: self.length_] = self.constant
        if time_series is not None:
            constant_series = pd.Series(
                data=constants, index=time_series.index
            ).to_frame()
        else:
            constant_series = pd.Series(data=constants).to_frame()

        return constant_series