from __future__ import annotations

import json
from datetime import date
from enum import Enum
from json import JSONDecodeError
from numbers import Number
from typing import Union, Optional, List, Any, Iterable, Dict

from citepy.validate import (
    type_validator,
    name_validator,
    date_validator,
    item_validator,
)

StrNum = Union[str, Number]
StrNumBool = Union[str, Number, bool]


class CslObject:
    _validator = None

    def __init__(self, **kwargs):
        self._check_types()

    def to_jso(self):
        out = py_to_jso(self.__dict__)
        self._validator.validate(out)
        return out

    @classmethod
    def from_jso(cls, jso) -> CslObject:
        cls._validator.validate(jso)
        return cls(**jso_to_py(jso))

    def _check_types(self):
        self.to_jso()
        # type_hints = get_type_hints(self)
        # for attr_name, type_hint in type_hints.items():
        #     with suppress(KeyError):  # Assume un-annotated parameters can be any type
        #         value = getattr(self, attr_name)
        #         if isinstance(type_hint, _SpecialForm):
        #             # No check for typing.Any, typing.Union, typing.ClassVar (without parameters)
        #             continue
        #         try:
        #             actual_type = type_hint.__origin__
        #         except AttributeError:
        #             actual_type = type_hint
        #         if isinstance(actual_type, _SpecialForm):
        #             # case of typing.Union[…] or typing.ClassVar[…]
        #             actual_type = type_hint.__args__
        #
        #         if not isinstance(value, actual_type):
        #             raise TypeError('Unexpected type for \'{}\' (expected {} but found {})'.format(name, type_hint, type(value)))

    def __str__(self):
        return json.dumps(self.to_jso())

    def __eq__(self, other):
        if isinstance(other, str):
            try:
                other_jso = json.loads(other)
            except JSONDecodeError:
                return False
        elif isinstance(other, CslObject):
            other_jso = other.to_jso()
        else:
            other_jso = other

        return self.to_jso() == other_jso


class CslType(Enum):
    ARTICLE = "article"
    ARTICLE_JOURNAL = "article-journal"
    ARTICLE_MAGAZINE = "article-magazine"
    ARTICLE_NEWSPAPER = "article-newspaper"
    BILL = "bill"
    BOOK = "book"
    BROADCAST = "broadcast"
    CHAPTER = "chapter"
    DATASET = "dataset"
    ENTRY = "entry"
    ENTRY_DICTIONARY = "entry-dictionary"
    ENTRY_ENCYCLOPEDIA = "entry-encyclopedia"
    FIGURE = "figure"
    GRAPHIC = "graphic"
    INTERVIEW = "interview"
    LEGAL_CASE = "legal_case"
    LEGISLATION = "legislation"
    MANUSCRIPT = "manuscript"
    MAP = "map"
    MOTION_PICTURE = "motion_picture"
    MUSICAL_SCORE = "musical_score"
    PAMPHLET = "pamphlet"
    PAPER_CONFERENCE = "paper-conference"
    PATENT = "patent"
    PERSONAL_COMMUNICATION = "personal_communication"
    POST = "post"
    POST_WEBLOG = "post-weblog"
    REPORT = "report"
    REVIEW = "review"
    REVIEW_BOOK = "review-book"
    SONG = "song"
    SPEECH = "speech"
    THESIS = "thesis"
    TREATY = "treaty"
    WEBPAGE = "webpage"

    def to_jso(self):
        out = self.value
        type_validator.validate(out)
        return out

    @classmethod
    def from_jso(cls, value) -> CslType:
        type_validator.validate(value)
        return CslType(value)


class CslName(CslObject):
    _validator = name_validator

    def __init__(
        self,
        family: Optional[str] = None,
        given: Optional[str] = None,
        dropping_particle: Optional[str] = None,
        non_dropping_particle: Optional[str] = None,
        suffix: Optional[str] = None,
        comma_suffix: Optional[StrNumBool] = None,
        static_ordering: Optional[StrNumBool] = None,
        literal: Optional[str] = None,
        parse_names: Optional[StrNumBool] = None,
    ):
        self.family: Optional[str] = family
        self.given: Optional[str] = given
        self.dropping_particle: Optional[str] = dropping_particle
        self.non_dropping_particle: Optional[str] = non_dropping_particle
        self.suffix: Optional[str] = suffix
        self.comma_suffix: Optional[StrNumBool] = comma_suffix
        self.static_ordering: Optional[StrNumBool] = static_ordering
        self.literal: Optional[str] = literal
        self.parse_names: Optional[StrNumBool] = parse_names

        super().__init__()


class CslDate(CslObject):
    _validator = date_validator

    def __init__(
        self,
        date_parts: Optional[List[List[StrNum]]] = None,
        season: Optional[StrNum] = None,
        circa: Optional[StrNumBool] = None,
        literal: Optional[str] = None,
        raw: Optional[str] = None,
    ):
        # outer has <=2 items, inner has <=3
        self.date_parts: Optional[List[List[StrNum]]] = date_parts
        self.season: Optional[StrNum] = season
        self.circa: Optional[StrNumBool] = circa
        self.literal: Optional[str] = literal
        self.raw: Optional[str] = raw

        super().__init__()

    @classmethod
    def from_date(cls, start, end=None):
        lsts = [[start.year, start.month, start.day]]
        if end is not None:
            lsts.append([end.year, end.month, end.day])
        return cls(lsts)


ValidName = Union[CslName, str, Dict[str, Any]]


def normalise_name(obj: ValidName) -> CslName:
    if isinstance(obj, CslName):
        return obj
    elif isinstance(obj, str):
        return CslName(literal=obj)
    elif isinstance(obj, dict):
        return CslName.from_jso(obj)
    else:
        raise TypeError(f"Don't know how to normalise name of type {type(obj)}")


def normalise_name_list(
    obj: Optional[Union[ValidName, Iterable[ValidName]]]
) -> Optional[List[CslName]]:
    if obj is None:
        return obj

    try:
        return [normalise_name(obj)]
    except TypeError:
        pass

    return [normalise_name(item) for item in obj]


ValidDate = Union[date, CslDate, Dict[str, Any], str, List[List[Number]]]


def normalise_date(obj: Optional[ValidDate]) -> Optional[CslDate]:
    if obj is None or isinstance(obj, CslDate):
        return obj

    if isinstance(obj, dict):
        return CslDate.from_jso(obj)

    if isinstance(obj, str):
        return CslDate(literal=obj)

    if isinstance(obj, Iterable):
        obj = list(obj)
        if all(isinstance(item, date) for item in obj):
            return CslDate.from_date(*obj)
        return CslDate(date_parts=obj)

    if isinstance(obj, date):
        return CslDate.from_date(obj)

    raise TypeError(f"Not sure how to normalise date from type {type(obj)}")


class CslItem(CslObject):
    _validator = item_validator

    def __init__(
        self,
        type: Union[str, CslType],
        id: StrNum,
        categories: Optional[List[str]] = None,
        language: Optional[str] = None,
        journal_abbreviation: Optional[str] = None,
        short_title: Optional[str] = None,
        author: Optional[List[ValidName]] = None,
        collection_editor: Optional[List[ValidName]] = None,
        composer: Optional[List[ValidName]] = None,
        container_author: Optional[List[ValidName]] = None,
        director: Optional[List[ValidName]] = None,
        editor: Optional[List[ValidName]] = None,
        editorial_director: Optional[List[ValidName]] = None,
        interviewer: Optional[List[ValidName]] = None,
        illustrator: Optional[List[ValidName]] = None,
        original_author: Optional[List[ValidName]] = None,
        recipient: Optional[List[ValidName]] = None,
        reviewed_author: Optional[List[ValidName]] = None,
        translator: Optional[List[ValidName]] = None,
        accessed: Optional[ValidDate] = None,
        container: Optional[ValidDate] = None,
        event_date: Optional[ValidDate] = None,
        issued: Optional[ValidDate] = None,
        original_date: Optional[ValidDate] = None,
        submitted: Optional[ValidDate] = None,
        abstract: Optional[str] = None,
        annote: Optional[str] = None,
        archive: Optional[str] = None,
        archive_location: Optional[str] = None,
        archive_place: Optional[str] = None,
        authority: Optional[str] = None,
        call_number: Optional[str] = None,
        chapter_number: Optional[str] = None,
        citation_number: Optional[str] = None,
        citation_label: Optional[str] = None,
        collection_number: Optional[str] = None,
        collection_title: Optional[str] = None,
        container_title: Optional[str] = None,
        container_title_short: Optional[str] = None,
        dimensions: Optional[str] = None,
        DOI: Optional[str] = None,
        edition: Optional[StrNum] = None,
        event: Optional[str] = None,
        event_place: Optional[str] = None,
        first_reference_note_number: Optional[str] = None,
        genre: Optional[str] = None,
        ISBN: Optional[str] = None,
        ISSN: Optional[str] = None,
        issue: Optional[StrNum] = None,
        jurisdiction: Optional[str] = None,
        keyword: Optional[str] = None,
        locator: Optional[str] = None,
        medium: Optional[str] = None,
        note: Optional[str] = None,
        number: Optional[StrNum] = None,
        number_of_pages: Optional[str] = None,
        number_of_volumes: Optional[StrNum] = None,
        original_publisher: Optional[str] = None,
        original_publisher_place: Optional[str] = None,
        original_title: Optional[str] = None,
        page: Optional[str] = None,
        page_first: Optional[str] = None,
        PMCID: Optional[str] = None,
        PMID: Optional[str] = None,
        publisher: Optional[str] = None,
        publisher_place: Optional[str] = None,
        references: Optional[str] = None,
        reviewed_title: Optional[str] = None,
        scale: Optional[str] = None,
        section: Optional[str] = None,
        source: Optional[str] = None,
        status: Optional[str] = None,
        title: Optional[str] = None,
        title_short: Optional[str] = None,
        URL: Optional[str] = None,
        version: Optional[str] = None,
        volume: Optional[StrNum] = None,
        year_suffix: Optional[StrNum] = None,
    ):
        self.type: CslType = CslType(type)
        self.id: StrNum = id
        self.categories: Optional[List[str]] = categories
        self.language: Optional[str] = language
        self.journal_abbreviation: Optional[str] = journal_abbreviation
        self.short_title: Optional[str] = short_title

        self.author: Optional[List[CslName]] = normalise_name_list(author)
        self.collection_editor: Optional[List[CslName]] = normalise_name_list(
            collection_editor
        )
        self.composer: Optional[List[CslName]] = normalise_name_list(composer)
        self.container_author: Optional[List[CslName]] = normalise_name_list(
            container_author
        )
        self.director: Optional[List[CslName]] = normalise_name_list(director)
        self.editor: Optional[List[CslName]] = normalise_name_list(editor)
        self.editorial_director: Optional[List[CslName]] = normalise_name_list(
            editorial_director
        )
        self.interviewer: Optional[List[CslName]] = normalise_name_list(interviewer)
        self.illustrator: Optional[List[CslName]] = normalise_name_list(illustrator)
        self.original_author: Optional[List[CslName]] = normalise_name_list(
            original_author
        )
        self.recipient: Optional[List[CslName]] = normalise_name_list(recipient)
        self.reviewed_author: Optional[List[CslName]] = normalise_name_list(
            reviewed_author
        )
        self.translator: Optional[List[CslName]] = normalise_name_list(translator)

        self.accessed: Optional[CslDate] = normalise_date(accessed)
        self.container: Optional[CslDate] = normalise_date(container)
        self.event_date: Optional[CslDate] = normalise_date(event_date)
        self.issued: Optional[CslDate] = normalise_date(issued)
        self.original_date: Optional[CslDate] = normalise_date(original_date)
        self.submitted: Optional[CslDate] = normalise_date(submitted)

        self.abstract: Optional[str] = abstract
        self.annote: Optional[str] = annote
        self.archive: Optional[str] = archive
        self.archive_location: Optional[str] = archive_location
        self.archive_place: Optional[str] = archive_place
        self.authority: Optional[str] = authority
        self.call_number: Optional[str] = call_number
        self.chapter_number: Optional[str] = chapter_number
        self.citation_number: Optional[str] = citation_number
        self.citation_label: Optional[str] = citation_label
        self.collection_number: Optional[str] = collection_number
        self.collection_title: Optional[str] = collection_title
        self.container_title: Optional[str] = container_title
        self.container_title_short: Optional[str] = container_title_short
        self.dimensions: Optional[str] = dimensions
        self.DOI: Optional[str] = DOI
        self.edition: Optional[StrNum] = edition
        self.event: Optional[str] = event
        self.event_place: Optional[str] = event_place
        self.first_reference_note_number: Optional[str] = first_reference_note_number
        self.genre: Optional[str] = genre
        self.ISBN: Optional[str] = ISBN
        self.ISSN: Optional[str] = ISSN
        self.issue: Optional[StrNum] = issue
        self.jurisdiction: Optional[str] = jurisdiction
        self.keyword: Optional[str] = keyword
        self.locator: Optional[str] = locator
        self.medium: Optional[str] = medium
        self.note: Optional[str] = note
        self.number: Optional[StrNum] = number
        self.number_of_pages: Optional[str] = number_of_pages
        self.number_of_volumes: Optional[StrNum] = number_of_volumes
        self.original_publisher: Optional[str] = original_publisher
        self.original_publisher_place: Optional[str] = original_publisher_place
        self.original_title: Optional[str] = original_title
        self.page: Optional[str] = page
        self.page_first: Optional[str] = page_first
        self.PMCID: Optional[str] = PMCID
        self.PMID: Optional[str] = PMID
        self.publisher: Optional[str] = publisher
        self.publisher_place: Optional[str] = publisher_place
        self.references: Optional[str] = references
        self.reviewed_title: Optional[str] = reviewed_title
        self.scale: Optional[str] = scale
        self.section: Optional[str] = section
        self.source: Optional[str] = source
        self.status: Optional[str] = status
        self.title: Optional[str] = title
        self.title_short: Optional[str] = title_short
        self.URL: Optional[str] = URL
        self.version: Optional[str] = version
        self.volume: Optional[StrNum] = volume
        self.year_suffix: Optional[StrNum] = year_suffix

        super().__init__()


jso_to_py_names = {
    "article-journal": "article_journal",
    "article-magazine": "article_magazine",
    "article-newspaper": "article_newspaper",
    "entry-dictionary": "entry_dictionary",
    "entry-encyclopedia": "entry_encyclopedia",
    "post-weblog": "post_weblog",
    "review-book": "review_book",
    "dropping-particle": "dropping_particle",
    "non-dropping-particle": "non_dropping_particle",
    "comma-suffix": "comma_suffix",
    "static-ordering": "static_ordering",
    "parse-names": "parse_names",
    "journalAbbreviation": "journal_abbreviation",
    "shortTitle": "short_title",
    "collection-editor": "collection_editor",
    "container-author": "container_author",
    "editorial-director": "editorial_director",
    "original-author": "original_author",
    "reviewed-author": "reviewed_author",
    "event-date": "event_date",
    "original-date": "original_date",
    "archive-location": "archive_location",
    "archive-place": "archive_place",
    "call-number": "call_number",
    "chapter-number": "chapter_number",
    "citation-number": "citation_number",
    "citation-label": "citation_label",
    "collection-number": "collection_number",
    "collection-title": "collection_title",
    "container-title": "container_title_short",
    "date-parts": "date_parts",
    "event-place": "event_place",
    "first-reference-note-number": "first_reference_note_number",
    "number-of-pages": "number_of_pages",
    "number-of-volumes": "number_of_volumes",
    "original-publisher": "original_publisher",
    "original-place": "original_place",
    "original-title": "original_title",
    "page-first": "page_first",
    "publisher-place": "publisher_place",
    "reviewed-title": "reviewed_title",
    "title-short": "title_short",
    "year-suffix": "year_suffix",
}
py_to_jso_names = {v: k for k, v in jso_to_py_names.items()}


class CslNameList:
    @classmethod
    def from_jso(cls, lst):
        return [CslName.from_jso(item) for item in lst]


name_to_class = {
    "type": CslType,
    "author": CslNameList,
    "collection-editor": CslNameList,
    "composer": CslNameList,
    "container-author": CslNameList,
    "director": CslNameList,
    "editor": CslNameList,
    "editorial-director": CslNameList,
    "interviewer": CslNameList,
    "illustrator": CslNameList,
    "original-author": CslNameList,
    "recipient": CslNameList,
    "reviewed-author": CslNameList,
    "translator": CslNameList,
    "container": CslDate,
    "event-date": CslDate,
    "issued": CslDate,
    "original-date": CslDate,
    "submitted": CslDate,
}


def py_to_jso(obj: Any):
    if obj is None:
        return obj

    if isinstance(obj, (bool, Number)):
        return obj

    if isinstance(obj, str):
        return py_to_jso_names.get(obj, obj)

    if isinstance(obj, list):
        return [py_to_jso(item) for item in obj]

    if isinstance(obj, dict):
        d = dict()
        for k, v in obj.items():
            if v is None:
                continue
            key = py_to_jso(k)
            if isinstance(key, Number):
                key = str(Number)
            if not isinstance(key, str):
                raise TypeError("Key of dict is not a string: cannot convert to JSO")
            d[key] = py_to_jso(v)

        return {str(py_to_jso(k)): py_to_jso(v) for k, v in d.items()}

    return obj.to_jso()


def jso_to_py(jso: Any):
    if jso is None:
        return jso

    if isinstance(jso, (bool, Number)):
        return jso

    if isinstance(jso, str):
        return jso_to_py_names.get(jso, jso)

    if isinstance(jso, list):
        return [jso_to_py(item) for item in jso]

    if isinstance(jso, dict):
        d = dict()
        for k, v in jso.items():
            key = jso_to_py_names.get(k, k)
            try:
                val = name_to_class[k].from_jso(v)
            except KeyError:
                val = v
            d[key] = val
        return d

    raise ValueError("Given object is not a valid CSL-JSON object")
