from dataclasses import dataclass
import markdown
import mistune
import os
import web
import PIL.Image

import typing

PER_PAGE = 16


def slugify(string):
    def process(char):
        if char in "- \n\t\f":
            return "-"
        elif char.isalpha():
            return char.lower()
        else:
            return ""

    return "".join(process(c) for c in string)[:40]


class Image(typing.NamedTuple):
    id: int
    image: str

    def thumb(self):
        return self.image[:-4] + "_thumb" + self.image[-4:]


class PageContent:
    def __init__(self, source):
        self.source = source

    def rendered(self):
        return markdown.markdown(self.source)

    def snipped(self):
        if len(self.source) < 256:
            return self.source
        else:
            return self.source[:256] + "..."

    def to_slate(self):
        return mistune.create_markdown(renderer=mistune.AstRenderer())(self.source)


@dataclass
class PageRef:
    page: int

    def __iter__(self):
        return [self]


@dataclass
class Design:
    title: str
    images: typing.List[Image]
    description: PageContent
    category: str
    id: int

    def id_str(self):
        return f"{self.id:05}"

    def slug(self):
        return slugify(self.title)

    def thumbnail(self):
        if self.images:
            return self.images[0].thumb()

    def tags(self, db):
        return db.get_tags_for_design(self.id)

    def category_list(self, db):
        categories = db.all_categories()
        return [
            {"name": c.name, "selected": c.name == self.category}
            for c in categories.elements
        ]

    @classmethod
    def list(cls, query_results) -> typing.List["Design"]:
        return [
            cls(
                title=d.title,
                images=[Image(image=d.image, id=0)],
                description=d.description,
                category=d.cat_name,
                id=d.id,
            )
            for d in query_results
        ]

    def to_json(self, db):
        return {
            "id": self.id,
            "title": self.title,
            "tags": self.tags(db),
            "categories": self.category_list(db),
            "description": self.description.source,
            "slate": self.description.to_slate(),
            "photos": self.images,
        }


@dataclass
class Paginated:
    next_page: typing.Optional[dict]
    prev_page: typing.Optional[dict]
    last_page: int
    contents: typing.List[Design]

    @classmethod
    def paginate(
        cls, offset: int, total: int, contents: typing.List[Design],
    ) -> "Paginated":
        last_page = total // PER_PAGE
        next_page = PageRef(page=offset + 1) if offset < last_page else None
        prev_page = PageRef(page=offset - 1) if offset > 0 else None
        return cls(
            next_page=next_page,
            prev_page=prev_page,
            last_page=last_page,
            contents=contents,
        )


@dataclass
class Tag:
    tag: str
    count: int

    def pretty(self):
        return " ".join(w.capitalize() for w in self.tag.split())


@dataclass
class Link:
    url: str
    name: str

    @classmethod
    def from_category(cls, c):
        return cls(name=c.nicename, url=f"/category/{c.name}")

    @classmethod
    def from_tag(cls, t):
        nicename = " ".join(w.capitalize() for w in t.tag_name.split())
        return cls(name=f"{nicename} ({t.n})", url=f"/tag/{t.tag_name}")


@dataclass
class LinkList:
    elements: typing.List[Link]


THUMB_SIZE = (100, 100)


class DB:
    def __init__(self, per_page=16):
        self._db = web.database(dbn="sqlite", db="frony.db")
        self.per_page = PER_PAGE
        self.categories = {}
        self.photo_num = self.get_max_photo_num() + 1

    def get_max_photo_num(self):
        n = 0
        for file in os.listdir(os.path.join("static", "photos")):
            try:
                n = max(int(file[:5]), n)
            except:
                pass
        return n

    def get_category_id(self, nm):
        id = list(self._db.where("categories", name=nm))
        return id and id[0].id

    def all_categories(self):
        categories = self._db.select("categories")
        return LinkList(elements=[Link.from_category(c) for c in categories])

    def get_category_name(self, id):
        if id in self.categories:
            return self.categories[id]
        else:
            nm = list(self._db.where("categories", id=id))
            if nm:
                self.categories[id] = nm[0].name
            return nm and nm[0].name or "unknown"

    def get_design(self, id):
        d = list(self._db.where("designs", id=id))
        if not d:
            return None
        else:
            d = d[0]
        return Design(
            title=d.title,
            images=self.get_all_photos_for_design(d.id),
            description=PageContent(d.description),
            category=self.get_category_name(d.category),
            id=d.id,
        )

    def put_design(self, id, title, description, category):
        description = description.replace("\r\n", "\n")
        self._db.update(
            "designs",
            where="id = $id",
            vars=dict(id=id),
            title=title,
            description=description,
            category=category,
        )

    def get_photo_by_id(self, id):
        try:
            photo = self._db.where("photos", id=id)[0]
            return Image(image=photo.filename, id=photo.id)
        except:
            return None

    def get_photo_by_filename(self, filename):
        try:
            photo = self._db.where("photos", filename=filename)[0]
            return Image(image=photo.filename, id=photo.id)
        except:
            return None

    def get_picture(self, id):
        try:
            photo = self._db.where("photos", design_id=id, limit=1)[0]
            return Image(image=photo.filename, id=photo.id)
        except:
            return None

    def get_all_photos(self, offset=0, pp=None):
        if not pp:
            pp = self.per_page
        return [
            Image(image=l.filename, id=l.id)
            for l in self._db.select(
                "photos", offset=offset * pp, limit=pp, order="id DESC"
            )
        ]

    def get_all_photos_for_design(self, id):
        return list(
            Image(image=d.filename, id=d.id)
            for d in self._db.where("photos", design_id=id)
        )

    def get_all(self, offset=0):
        ds = self._db.query(
            """select d.title, d.description, d.id, c.name as cat_name,
                 (select filename from photos where photos.design_id = d.id limit 1) as image
            from designs d, categories c
            where d.category = c.id
            order by d.id desc
            limit $per_page offset $offset""",
            vars={"per_page": PER_PAGE, "offset": offset},
        )
        total = self._db.query("select count(*) as c from designs")
        return Paginated.paginate(offset, total[0].c, Design.list(ds))

    def get_designs_by_category(self, cat, offset=0):
        ds = self._db.query(
            """select d.title, d.description, d.id, c.name as cat_name,
                 (select filename from photos where photos.design_id = d.id limit 1) as image
            from designs d, categories c
            where d.category = c.id
            and c.name = $cat
            order by d.id desc
            limit $per_page offset $offset""",
            vars={"cat": cat, "per_page": PER_PAGE, "offset": offset},
        )
        total = self._db.query(
            """select count(*) as c from designs d, categories c
            where d.category = c.id and c.name = $cat""",
            vars={"cat": cat},
        )
        return Paginated.paginate(offset, total[0].c, Design.list(ds))

    def get_designs_by_category_and_tag(self, cat, tag, offset=0):
        ds = self._db.query(
            """select d.title, d.description, d.id, c.name as cat_name,
                 (select filename from photos where photos.design_id = d.id limit 1) as image
               from designs d, tags t, categories c
               where t.tag_name = $tag
               and d.category=c.id
               and c.name = $cat
               and d.id = t.design_id
               order by d.id desc
               limit $per_page offset $offset""",
            vars=dict(
                tag=tag, cat=cat, offset=offset * self.per_page, per_page=self.per_page
            ),
        )
        total = self._db.query(
            """select count(*) as c from designs, tags, categories
            where designs.category = categories.id
            and categories.name = $cat
            and tags.tag_name = $tag
            and designs.id = tags.design_id""",
            vars=dict(tag=tag, cat=cat),
        )
        return Paginated.paginate(offset, total[0].c, Design.list(ds))

    def new_design(self):
        new_id = self._db.query("select max(id) as n from designs")[0].n + 1
        self._db.insert(
            "designs", id=new_id, title="New Design", description="", category=0
        )
        return new_id

    def delete_design(self, id):
        self._db.delete("designs", where="id = $id", vars=dict(id=id))

    def add_photo(self, file, d_id):
        self.photo_num = self.get_max_photo_num() + 1
        new_id = self._db.query("select max(id) as n from photos")[0].n + 1
        new_num = self.photo_num
        self.photo_num += 1
        extension = file.filename[-3:]
        name = "{0:05}.{1}".format(new_num, extension)
        thumb = "{0:05}_thumb.{1}".format(new_num, extension)

        img = PIL.Image.open(file.stream)
        img.thumbnail((400, 400), PIL.Image.ANTIALIAS)
        img.save(os.path.join(os.getcwd(), "static", "photos", name))
        img.thumbnail(THUMB_SIZE, PIL.Image.ANTIALIAS)
        img.save(os.path.join("static", "photos", thumb))
        self._db.insert("photos", id=new_id, filename=name, design_id=d_id)
        return new_id

    def delete_photo(self, photo_name):
        self._db.delete(
            "photos", where="filename = $filename", vars=dict(filename=photo_name)
        )

    def max_photo_page_ranges(self):
        num_designs = self._db.query("select count(*) as n from photos")[0].n
        return (0, (num_designs // self.per_page))

    def max_page_ranges(self):
        num_designs = self._db.query("select count(*) as n from designs")[0].n
        return (0, (num_designs // self.per_page))

    def max_page_range_for_category(self, cat):
        cat_id = self.get_category_id(cat)
        num_designs = self._db.query(
            "select count(*) as n from designs where category = $cat",
            vars=dict(cat=cat_id),
        )[0].n
        return (0, 1 + (num_designs // self.per_page))

    def get_all_tags(self):
        tags = self._db.query(
            """select t.tag_name, count(*) as n from designs d, tags t
            where d.id = t.design_id group by t.tag_name"""
        )
        return LinkList(elements=sorted([Link.from_tag(t) for t in tags]))

    def get_designs_by_tag(self, tag, offset=0):
        ds = self._db.query(
            """select d.title, d.description, d.id, c.name as cat_name,
                 (select filename from photos where photos.design_id = d.id limit 1) as image
               from designs d, tags t, categories c
               where t.tag_name = $tag
               and d.id = t.design_id
               and d.category = c.id
               order by d.id desc
               limit $per_page offset $offset""",
            vars=dict(tag=tag, offset=offset * self.per_page, per_page=self.per_page),
        )
        total = self._db.query(
            """select count(*) as c from designs d, tags t
            where t.tag_name = $tag and d.id = t.design_id""",
            vars={"tag": tag},
        )
        return Paginated.paginate(offset, total[0].c, Design.list(ds))

    def get_tags_for_design(self, design_id):
        return [
            t.tag_name
            for t in self._db.select(
                "tags",
                what="tag_name",
                where="design_id = $d_id",
                vars=dict(d_id=design_id),
            )
        ]

    def process_tag_list(self, design_id, tag_string):
        new_tag_id = (
            lambda: (self._db.query("select max(id) as n from tags")[0].n or 0) + 1
        )
        tag_list = tag_string.lower().split()
        self._db.delete("tags", where="design_id = $d_id", vars=dict(d_id=design_id))
        for t in tag_list:
            self._db.insert("tags", id=new_tag_id(), tag_name=t, design_id=design_id)

    def max_page_range_for_tag(self, tag):
        num_designs = self._db.query(
            "select count(*) as n from tags where tag_name = $t", vars=dict(t=tag)
        )[0].n
        return (0, (num_designs // self.per_page))

    def num_for_tag(self, tag):
        return self._db.query(
            "select count(*) as n from tags where tag_name = $t", vars={"t": tag}
        )[0].n

    def get_clacker(self, new_visitor=None):
        if new_visitor is not None:
            exists = self._db.query(
                "select count(*) as n from visits where visitor = $v",
                vars={"v": new_visitor},
            )[0].n
        else:
            exists = 1
        total = self._db.query("select count(*) as n from visits")[0].n
        if exists == 0 and new_visitor:
            self._db.insert("visits", visitor=new_visitor)
            return total + 1
        else:
            return total
