import datetime
from dateutil import parser as dtparser

import common

class DictConverter:

    def __init__(self, factories):
        self.factories = factories

    def convert(self, item, lang, key = None):
        if isinstance(item, dict):
            res = {}
            for k in item.keys():
                res[k] = self.convert(item[k], lang, k)
            return res
        elif isinstance(item, list):
            res = []
            for i in item:
                res.append(self.convert(i, lang))
            return res

        # Replace specific string keywords with their referenced objects
        elif isinstance(item, str) and key == "link":
            return self.factories['link'].get_by_raw(item, lang)
        elif isinstance(item, str) and key == "tag":
            return self.factories['tag'].get(item, lang)
        elif isinstance(item, str) and key == "file":
            return self.factories['file'].get(item)
        elif isinstance(item, str) and key == "page":
            return self.factories['page'].get(item, lang)
        #elif isinstance(item, str) and key == "slug": # Doesn't work because 'slug' is used to override the slug of a page in its metadata.
        #    return self.factories['page'].get(item, lang)
        else:
            return item



class LocalizedConfigFactory(DictConverter):
    def __init__(self, config, factories):
        super().__init__(factories)
        self.config = config
        self.store = {}

    def get(self, lang):
        if lang not in self.store:
            self.store[lang] = self.convert(self.config, lang, None)
        return self.store[lang]

class FileFactory:
    def __init__(self, config, factories):
        self.config = config
        self.factories = factories
        self.store = {}
    def get(self, filename):
        if filename not in self.store:
            self.store[filename] = File(filename, self.config, self.factories)
        return self.store[filename];
    def all(self):
        return self.store
class File:
    def __init__(self, filename, config, factories):
        self.name = filename
        self._config = config
        self._factories = factories
        self.initialized = False

    def init(self, rawcontents, subpathlist):
        if self.initialized:
            raise Exception("Tried initializing file twice. Perhaps two files with same name exist? ", self.name)
        self.initialized = True
        self.rawcontents = rawcontents
        self.subpathlist = subpathlist
        self.link = self._factories['link'].get_by_type("file", self.name)

class PageFactory:
    def __init__(self, config, factories):
        self.config = config
        self.factories = factories
        self.store = {}
        for lang in self.config['lang']['supported']:
            self.store[lang] = {}

    def has(self, slug, lang):
        return (slug in self.store[lang])

    def get(self, slug, lang):
        if not self.has(slug, lang):
            self.store[lang][slug] = Page(slug, lang, self.factories)
        return self.store[lang][slug];

    def all(self):
        return self.store
class Page:
    def __init__(self, slug, lang, factories):
        self.slug = slug
        self.lang = lang
        self._config = None
        self._factories = factories
        self.initialized = False
        self.status = "uninitialized"
        self.title = "N/A"
        self._metadata = {}
        self.content = []

    def init(self, filename, subpath, raw, metadata, content, title, category, date_created, date_modified, authors, last_modification_author, status):
        self.initialized = True
        self.filename =  filename
        self.subpath =  subpath
        self.raw =  raw
        dictconverter = DictConverter(self._factories)
        self._metadata =  dictconverter.convert(metadata, self.lang)
        self.content =  content
        self.title =  title
        self.category = self._factories['tag'].get(category, self.lang)
        self.date_created =  Date(date_created, self.config)
        self.date_modified =  Date(date_modified, self.config)
        self.status =  status

        # authors
        self.authors = set()
        for local_part, domain, name in authors:
            self.authors.add(self._factories['author'].get(local_part + '@' + domain, name))
        if last_modification_author:
            self.last_modification_author = self._factories['author'].get(last_modification_author[0] + '@' + last_modification_author[1], last_modification_author[2])
        else:
            self.last_modification_author = None
        #print(authors)
        #print(last_modification_author)

        # category
        self.category.reg_page(self, True)

        # url
        #self.url = self.lang + '/' + self.category.name + '/' + self.slug + ".html"
        self.link = self._factories['link'].get_by_type("slug", self.slug, self.lang)

        gitlabsettings = self._config["gitlab"]

        raw_edit_url = gitlabsettings["edit_url"]["prefix"]
        raw_edit_url += '/'.join(self.subpath)
        if len(self.subpath):
            raw_edit_url += '/'
        raw_edit_url += self.filename
        raw_edit_url += gitlabsettings["edit_url"]["suffix"]
        self.edit_url = self._factories['link'].get_by_raw(raw_edit_url, self.lang)

        raw_view_url = gitlabsettings["view_url"]["prefix"]
        raw_view_url += '/'.join(self.subpath)
        if len(self.subpath):
            raw_view_url += '/'
        raw_view_url += self.filename
        raw_view_url += gitlabsettings["view_url"]["suffix"]
        self.view_url = self._factories['link'].get_by_raw(raw_view_url, self.lang)

        raw_history_url = gitlabsettings["history_url"]["prefix"]
        raw_history_url += '/'.join(self.subpath)
        if len(self.subpath):
            raw_history_url += '/'
        raw_history_url += self.filename
        raw_history_url += gitlabsettings["history_url"]["suffix"]
        self.history_url = self._factories['link'].get_by_raw(raw_history_url, self.lang)


    def get_config(self):
        if not self._config:
            self._config = self._factories['config'].get(self.lang)
        return self._config
    config = property(get_config, None, None)


    def get_metadata(self):
        deflang = self.config['lang']['default']
        if self.lang == deflang:
            return self._metadata
        defpage = self._factories['page'].get(self.slug, deflang)
        res = common.combine(defpage.metadata, self._metadata)
        return res
    metadata = property(get_metadata, None, None)

    def get_tags(self):
        if not self.initialized:
            raise Exception("Page not initialized.")
        res = []
        if 'tags' in self.metadata:
            rawtags = self.tags_str_to_list(self.metadata['tags'])
            for t in rawtags:
                if (t != self.category.name):
                    tag = self._factories['tag'].get(t, self.lang)
                    tag.reg_page(self)
                    res.append(tag)
        res.append(self.category) # the category is also a default tag
        return res
    tags = property(get_tags, None, None)

    def get_relevance(self):
        res = {}
        res['is_relevant'] = False
        res['was_relevant'] = False
        res['will_be_relevant'] = False
        if 'relevant' in self.metadata:
            lrel = self.metadata['relevant']
            if not isinstance(lrel, dict):
                prio = lrel
                lrel = {}
                lrel['prio'] = prio

            if 'prio' in lrel:
                res['prio']= lrel['prio']
            else:
                res['prio']= 0

            start = Date("min", self.config)
            if 'start' in lrel:
                start = Date(lrel['start'], self.config)
            res['start'] = start

            end = Date("max", self.config)
            if 'end' in lrel:
                end = Date(lrel['end'], self.config)
            res['end'] = end

            build_date = Date("now", self.config)
            if (build_date > end):
                res['was_relevant'] = True
            elif (build_date < start):
                res['will_be_relevant'] = True
            else:
                res['is_relevant'] = True
        return res
    relevance = property(get_relevance, None, None)

    def get_template(self):
        if 'template' in self.metadata:
            return self.metadata['template']
        else:
            return self.config['theme']['default_template']
    template = property(get_template, None, None)


    def tags_str_to_list(self, s):
        if not self.initialized:
            raise Exception("Page not initialized.")

        res = []
        l = s.split(',')
        for t in l:
            t = t.lower()
            t = t.lstrip()
            t = t.rstrip()
            if len(t) > 0:
                res.append(t)
        return res


    def get_prio(self):
        if 'prio' in self.metadata:
            return self.metadata['prio']
        else:
            return 0
    prio = property(get_prio, None, None)

    def __lt__(self, other): # for sorting by date modified (default)
        if not self.initialized:
            raise Exception("Page not initialized.")
        return self.date_modified < other.date_modified

class TagFactory:
    def __init__(self, config, factories):
        self.config = config
        self.factories = factories
        self.store = {}
        for lang in self.config['lang']['supported']:
            self.store[lang] = {}
    def get(self, name, lang):
        if not isinstance(name, str):
            raise Exception("name is not a string", name, type(name))
        if lang == None:
            raise Exception("cannot get None lang for tag", name)
        if name not in self.store[lang]:
            self.store[lang][name] = Tag(name, lang, self.factories)
        return self.store[lang][name];
    def all(self):
        return self.store
class Tag:
    def __init__(self, name, lang, factories):
        self.name = name
        self.lang = lang
        self._config = None
        self.pages_tuple = []
        self.is_category = False
        self._link = None
        self._factories = factories
        self._pages_cache = []


    def get_config(self):
        if not self._config:
            self._config = self._factories['config'].get(self.lang)
        return self._config
    config = property(get_config, None, None)

    def get_link(self):
        if not self._link:
            self._link = self._factories['link'].get_by_type("tag", self.name, self.lang)
        return self._link
    link = property(get_link, None, None)

    def get_page(self):
        if self._factories['page'].has('tag-' + self.name, self.lang):
            return self._factories['page'].get('tag-' + self.name, self.lang)
        else:
            return None
    page = property(get_page, None, None)

    def get_color(self):
        if self.page and 'color' in self.page.metadata:
            return self.page.metadata['color']
        else:
            return self.config['theme']['default_tag_color']
    color = property(get_color, None, None)

    def get_title(self):
        if self.page:
            return self.page.title
        else:
            return self.name
    title = property(get_title, None, None)

    def reg_page(self, page, is_category = False):
        if page == None:
            return
        if page.status != "published":
            return
        if page.lang != self.lang:
            raise Exception("Page has different lang than Tag.", page.lang, tag.lang)
        if page not in self.pages:
            self.pages_tuple.append((page.prio, page))
            self.pages_tuple.sort(key=lambda x: x[0])
            self.pages_tuple.reverse()
        if is_category:
            self.is_category = is_category


    def get_pages(self):
        if len(self._pages_cache) != len(self.pages_tuple):
            res = []
            for prio, page in self.pages_tuple:
                res.append(page)
            self._pages_cache = res
        return self._pages_cache
    pages = property(get_pages, None, None)

class Date:
    def __init__(self, dt, config):
        if isinstance(dt, str):
            if dt == "min":
                dt = dtparser.parse("1970-01-01")
            elif dt == "max":
                dt = dtparser.parse("9999-01-01")
            elif dt == "now":
                dt = datetime.datetime.now()
            else:
                dt = dtparser.parse(dt)
        if not isinstance(dt, datetime.datetime):
                dt = datetime.datetime(dt.year, dt.month, dt.day)
        self.dt = dt
        #self.config = config
        # TODO various formats
    def isoformat(self):
        return self.dt.isoformat()

    def strftime(self, formatstr):
        # https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
        return self.dt.strftime(formatstr)

    def __lt__(self, other):
        return self.dt.timestamp() < other.dt.timestamp()

class AuthorFactory:
    def __init__(self, config, factories):
        self.config = config
        self.factories = factories
        self.store = {}
    def get(self, email, name = None):
        if email not in self.store:
            self.store[email] = Author(email)
        author = self.store[email]
        author.reg_name(name)
        return author;
class Author:
    def __init__(self, email):
        self.email = email
        self.names = set()

        split = email.split('@')
        # TODO proper email validation
        if len(split) != 2:
            raise Exception("Invalid email address: ", email)
        self.email_local_part = split[0]
        self.email_domain = split[1]
        # TODO md5 hash etc.

    def reg_name(self, name):
        if name == None:
            return
        self.names.add(name)

class LinkFactory:
    def __init__(self, config, factories):
        self.config = config
        self.factories = factories
        self.store = {}
    def get_by_raw(self, rawurl, deflang):
        link = Link(rawurl, self.factories, deflang) # this is dirty
        lang = link.lang
        if lang == None:
            raise Exception("Link lang is None", lang, deflang, link.raw)
        if lang not in self.store:
            self.store[lang] = {}
        if rawurl not in self.store[lang]:
            self.store[lang][rawurl] = link
        return self.store[lang][rawurl];
    def get_by_type(self, reftype, refid, reflang = None):
        rawurl = reftype + ':' + refid
        if reflang:
            rawurl += ':' + reflang
        else:
            reflang = self.config['lang']['default']
        return self.get_by_raw(rawurl=rawurl, deflang=reflang)
class Link:
    def __init__(self, rawurl, factories, deflang):
        self.raw = rawurl
        self._factories = factories
        self.is_external = False
        self.lang = deflang
        self.type = None
        self.alias = None

        urlasplit = rawurl.split('#')

        self.anchor = None
        if len(urlasplit) == 2:
            self.anchor = urlasplit[1]

        urlwoa = urlasplit[0] # url without anchor
        if len(urlwoa) == 0:
            # rawurl: '#some-section'
            self._url = ""
            self.type = "local"
        else:
            components = urlwoa.split(':')
            if len(components) == 1:
                # rawurl: 'lageplan.uni-goettingen.de'
                self._url = "https://" + urlwoa
                self.is_external = True
            else:
                reftype = components[0]
                refid = components[1]
                reflang = deflang
                if len(components) > 2:
                    reflang = components[2]
                if reftype == "slug":
                    # rawurl: 'slug:some-page:de'
                    self._page = None
                    self._url = None
                    self.type = reftype
                    self.lang = reflang
                    self.refid = refid
                elif reftype == "tag":
                    # rawurl: 'tag:some-tag:de'
                    self._tag = None
                    self._url = None
                    self.type = reftype
                    self.lang = reflang
                    self.refid = refid
                elif reftype == "file":
                    # rawurl: 'file:logo.png'
                    self._file = None
                    self._url = None
                    self.type = reftype
                    self.lang = reflang
                    self.refid = refid
                else:
                    # rawurl: 'https://example.com'
                    self._url = urlwoa
                    self.is_external = True

    def get_page(self):
        if not self._page:
            self._page = self._factories['page'].get(self.refid, self.lang)
            # handle link/alias pages
            if 'link' in self._page.metadata:
                self.alias = self._page.metadata['link']
                if self.alias.type == "slug":
                    self.alias.get_page() # make sure that alias knows if it is an alias
        return self._page
    page = property(get_page, None, None)

    def get_tag(self):
        if not self._tag:
            self._tag = self._factories['tag'].get(self.refid, self.lang)
        return self._tag
    tag = property(get_tag, None, None)

    def get_file(self):
        if not self._file:
            self._file = self._factories['file'].get(self.refid)
        return self._file
    file = property(get_file, None, None)

    def get_category(self):
        if self.type == "slug":
            return self.page.category
        elif self.type == "tag":
            return self.tag
            #if self.tag.is_category:
            #    return self.tag
            #else:
            #    raise Exception("category can only be called on category tags")
        else:
            raise Exception("category is not allowed to be called here")
    category = property(get_category, None, None)

    def get_path(self):
        if self.type == "slug":
            return '/'.join([self.page.lang, self.page.category.name, self.page.slug + '.html'])
        elif self.type == "tag":
            return '/'.join([self.tag.lang, 'tag', self.tag.name + '.html'])
        elif self.type == "file":
            return '/'.join(['file', self.file.name])
        else:
            raise Exception("path is not allowed to be called here", self.type)
    path = property(get_path, None, None)

    def get_url(self):
        if self._url == None:
            self._url = self.path
        return self._url
    url = property(get_url, None, None)

    def get_urlwithanchor(self):
        if self.anchor:
            return self.url + "#" + self.anchor
        else:
            return self.url
    urlwithanchor = property(get_urlwithanchor, None, None)

    def get_rel(self):
        """
        Get the relation in the context of the rel html attribute for this link
        or None if no relation is known.
        https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel
        """
        if self.type != "slug":
            return None
        if "rel" not in self.page.metadata:
            return None
        return self.page.metadata["rel"]
    rel = property(get_rel, None, None)