"""A class to encapsulate ComicRack's ComicInfo.xml data."""
# Copyright 2012-2014 ComicTagger Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations

import logging
import xml.etree.ElementTree as ET
from typing import Any
from typing import TYPE_CHECKING

from comicapi import utils
from comicapi.genericmetadata import FileHash
from comicapi.genericmetadata import GenericMetadata
from comicapi.genericmetadata import PageMetadata
from comicapi.tags import Tag

if TYPE_CHECKING:
    from comicapi.archivers import Archiver

logger = logging.getLogger(f'comicapi.metadata.{__name__}')


class ComicInfoXml(Tag):
    enabled = True

    id = 'cix'

    def __init__(self, version: str) -> None:
        super().__init__(version)

        self.file = 'ComicInfo.xml'
        self.supported_attributes = {
            'original_hash',
            'series',
            'issue',
            'issue_count',
            'title',
            'volume',
            'genres',
            'description',
            'notes',
            'alternate_series',
            'alternate_number',
            'alternate_count',
            'story_arcs',
            'series_groups',
            'publisher',
            'imprint',
            'day',
            'month',
            'year',
            'language',
            'web_links',
            'format',
            'manga',
            'black_and_white',
            'maturity_rating',
            'critical_rating',
            'scan_info',
            'tags',
            'pages',
            'pages.bookmark',
            'pages.double_page',
            'pages.height',
            'pages.image_index',
            'pages.size',
            'pages.type',
            'pages.width',
            'page_count',
            'characters',
            'teams',
            'locations',
            'credits',
            'credits.person',
            'credits.role',
        }

    def supports_credit_role(self, role: str) -> bool:
        return role.casefold() in self._get_parseable_credits()

    def supports_tags(self, archive: Archiver) -> bool:
        return archive.supports_files()

    def has_tags(self, archive: Archiver) -> bool:
        return (
            self.supports_tags(archive)
            and self.file in archive.get_filename_list()
            and self._validate_bytes(archive.read_file(self.file))
        )

    def remove_tags(self, archive: Archiver) -> bool:
        return self.has_tags(archive) and archive.remove_file(self.file)

    def read_tags(self, archive: Archiver) -> GenericMetadata:
        if self.has_tags(archive):
            metadata = archive.read_file(self.file) or b''
            if self._validate_bytes(metadata):
                return self._metadata_from_bytes(metadata)
        return GenericMetadata()

    def read_raw_tags(self, archive: Archiver) -> str:
        if self.has_tags(archive):
            return ET.tostring(ET.fromstring(archive.read_file(self.file)), encoding='unicode', xml_declaration=True)
        return ''

    def write_tags(self, metadata: GenericMetadata, archive: Archiver) -> bool:
        if self.supports_tags(archive):
            xml = b''
            if self.has_tags(archive):
                xml = archive.read_file(self.file)
            return archive.write_file(self.file, self._bytes_from_metadata(metadata, xml))
        logger.warning('Archive (%s) does not support %s metadata', archive.name(), self.name())
        return False

    def name(self) -> str:
        return 'Comic Info XML'

    @classmethod
    def _get_parseable_credits(cls) -> list[str]:
        parsable_credits: list[str] = []
        parsable_credits.extend(GenericMetadata.writer_synonyms)
        parsable_credits.extend(GenericMetadata.penciller_synonyms)
        parsable_credits.extend(GenericMetadata.inker_synonyms)
        parsable_credits.extend(GenericMetadata.colorist_synonyms)
        parsable_credits.extend(GenericMetadata.letterer_synonyms)
        parsable_credits.extend(GenericMetadata.cover_synonyms)
        parsable_credits.extend(GenericMetadata.editor_synonyms)
        parsable_credits.extend(GenericMetadata.translator_synonyms)
        return parsable_credits

    def _metadata_from_bytes(self, string: bytes) -> GenericMetadata:
        root = ET.fromstring(string)
        return self._convert_xml_to_metadata(root)

    def _bytes_from_metadata(self, metadata: GenericMetadata, xml: bytes = b'') -> bytes:
        root = self._convert_metadata_to_xml(metadata, xml)
        return ET.tostring(root, encoding='utf-8', xml_declaration=True)

    def _convert_metadata_to_xml(self, metadata: GenericMetadata, xml: bytes = b'') -> ET.Element:
        # shorthand for the metadata
        md = metadata

        if xml:
            root = ET.fromstring(xml)
        else:
            # build a tree structure
            root = ET.Element('ComicInfo')
            root.attrib['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance'
            root.attrib['xmlns:xsd'] = 'http://www.w3.org/2001/XMLSchema'
        # helper func

        def assign(cr_entry: str, md_entry: Any) -> None:
            if md_entry:
                text = str(md_entry)
                if isinstance(md_entry, (list, set)):
                    text = ','.join(md_entry)
                et_entry = root.find(cr_entry)
                if et_entry is not None:
                    et_entry.text = text
                else:
                    ET.SubElement(root, cr_entry).text = text
            else:
                et_entry = root.find(cr_entry)
                if et_entry is not None:
                    root.remove(et_entry)

        # need to specially process the credits, since they are structured
        # differently than CIX
        credit_writer_list = []
        credit_penciller_list = []
        credit_inker_list = []
        credit_colorist_list = []
        credit_letterer_list = []
        credit_cover_list = []
        credit_editor_list = []
        credit_translator_list = []

        # first, loop thru credits, and build a list for each role that CIX
        # supports
        for credit in metadata.credits:
            if credit.role.casefold() in GenericMetadata.writer_synonyms:
                credit_writer_list.append(credit.person.replace(',', ''))

            if credit.role.casefold() in GenericMetadata.penciller_synonyms:
                credit_penciller_list.append(credit.person.replace(',', ''))

            if credit.role.casefold() in GenericMetadata.inker_synonyms:
                credit_inker_list.append(credit.person.replace(',', ''))

            if credit.role.casefold() in GenericMetadata.colorist_synonyms:
                credit_colorist_list.append(credit.person.replace(',', ''))

            if credit.role.casefold() in GenericMetadata.letterer_synonyms:
                credit_letterer_list.append(credit.person.replace(',', ''))

            if credit.role.casefold() in GenericMetadata.cover_synonyms:
                credit_cover_list.append(credit.person.replace(',', ''))

            if credit.role.casefold() in GenericMetadata.editor_synonyms:
                credit_editor_list.append(credit.person.replace(',', ''))

            if credit.role.casefold() in GenericMetadata.translator_synonyms:
                credit_translator_list.append(credit.person.replace(',', ''))

        assign('Series', md.series)
        assign('Number', md.issue)
        assign('Count', md.issue_count)
        assign('Title', md.title)
        assign('Volume', md.volume)
        assign('Genre', md.genres)
        assign('Summary', md.description)
        assign('Notes', md.notes)

        assign('AlternateSeries', md.alternate_series)
        assign('AlternateNumber', md.alternate_number)
        assign('AlternateCount', md.alternate_count)
        assign('StoryArc', md.story_arcs)
        assign('SeriesGroup', md.series_groups)

        assign('Publisher', md.publisher)
        assign('Imprint', md.imprint)
        assign('Day', md.day)
        assign('Month', md.month)
        assign('Year', md.year)
        assign('LanguageISO', md.language)
        assign('Web', ' '.join(u.url for u in md.web_links))
        assign('Format', md.format)
        assign('Manga', md.manga)
        assign('BlackAndWhite', 'Yes' if md.black_and_white else None)
        assign('AgeRating', md.maturity_rating)
        assign('CommunityRating', md.critical_rating)

        scan_info = md.scan_info or ''
        if md.original_hash:
            scan_info += f" sum:{md.original_hash}"
        assign('ScanInformation', scan_info)

        assign('Tags', md.tags)
        assign('PageCount', md.page_count)

        assign('Characters', md.characters)
        assign('Teams', md.teams)
        assign('Locations', md.locations)
        assign('Writer', credit_writer_list)
        assign('Penciller', credit_penciller_list)
        assign('Inker', credit_inker_list)
        assign('Colorist', credit_colorist_list)
        assign('Letterer', credit_letterer_list)
        assign('CoverArtist', credit_cover_list)
        assign('Editor', credit_editor_list)
        assign('Translator', credit_translator_list)

        #  loop and add the page entries under pages node
        pages_node = root.find('Pages')
        if pages_node is not None:
            pages_node.clear()
        else:
            pages_node = ET.SubElement(root, 'Pages')

        for page in md.pages:
            page_node = ET.SubElement(pages_node, 'Page')
            page_node.attrib = {'Image': str(page.display_index)}
            if page.bookmark:
                page_node.attrib['Bookmark'] = page.bookmark
            if page.type:
                page_node.attrib['Type'] = page.type

            if page.double_page is not None:
                page_node.attrib['DoublePage'] = str(page.double_page)
            if page.height is not None:
                page_node.attrib['ImageHeight'] = str(page.height)
            if page.byte_size is not None:
                page_node.attrib['ImageSize'] = str(page.byte_size)
            if page.width is not None:
                page_node.attrib['ImageWidth'] = str(page.width)
            page_node.attrib = dict(sorted(page_node.attrib.items()))

        ET.indent(root)

        return root

    def _convert_xml_to_metadata(self, root: ET.Element) -> GenericMetadata:
        if root.tag != 'ComicInfo':
            raise Exception('Not a ComicInfo file')

        def get(name: str) -> str | None:
            tag = root.find(name)
            if tag is None:
                return None
            return tag.text

        md = GenericMetadata()

        md.series = utils.xlate(get('Series'))
        md.issue = utils.xlate(get('Number'))
        md.issue_count = utils.xlate_int(get('Count'))
        md.title = utils.xlate(get('Title'))
        md.volume = utils.xlate_int(get('Volume'))
        md.genres = set(utils.split(get('Genre'), ','))
        md.description = utils.xlate(get('Summary'))
        md.notes = utils.xlate(get('Notes'))

        md.alternate_series = utils.xlate(get('AlternateSeries'))
        md.alternate_number = utils.xlate(get('AlternateNumber'))
        md.alternate_count = utils.xlate_int(get('AlternateCount'))
        md.story_arcs = utils.split(get('StoryArc'), ',')
        md.series_groups = utils.split(get('SeriesGroup'), ',')

        md.publisher = utils.xlate(get('Publisher'))
        md.imprint = utils.xlate(get('Imprint'))
        md.day = utils.xlate_int(get('Day'))
        md.month = utils.xlate_int(get('Month'))
        md.year = utils.xlate_int(get('Year'))
        md.language = utils.xlate(get('LanguageISO'))
        md.web_links = utils.split_urls(utils.xlate(get('Web')))
        md.format = utils.xlate(get('Format'))
        md.manga = utils.xlate(get('Manga'))
        md.maturity_rating = utils.xlate(get('AgeRating'))
        md.critical_rating = utils.xlate_float(get('CommunityRating'))

        scan_info_list = (utils.xlate(get('ScanInformation')) or '').split()
        for word in scan_info_list.copy():
            if not word.startswith('sum:'):

                continue

            original_hash = FileHash.parse(word[4:])
            if original_hash:
                md.original_hash = original_hash
                scan_info_list.remove(word)
                break
        if scan_info_list:
            md.scan_info = ' '.join(scan_info_list)

        md.tags = set(utils.split(get('Tags'), ','))
        md.page_count = utils.xlate_int(get('PageCount'))

        md.characters = set(utils.split(get('Characters'), ','))
        md.teams = set(utils.split(get('Teams'), ','))
        md.locations = set(utils.split(get('Locations'), ','))

        tmp = utils.xlate(get('BlackAndWhite'))
        if tmp is not None:
            md.black_and_white = tmp.casefold() in {'yes', 'true', '1'}

        # Now extract the credit info
        for n in root:
            if n.tag in {'Writer', 'Penciller', 'Inker', 'Colorist', 'Letterer', 'Editor'} and n.text is not None:
                for name in utils.split(n.text, ','):
                    md.add_credit(name.strip(), n.tag)

            if n.tag == 'CoverArtist' and n.text is not None:
                for name in utils.split(n.text, ','):
                    md.add_credit(name.strip(), 'Cover')

        # parse page data now
        pages_node = root.find('Pages')
        if pages_node is not None:
            for i, page in enumerate(pages_node):
                p: dict[str, Any] = page.attrib
                md_page = PageMetadata(
                    filename='',  # cix doesn't record the filename it just assumes it's always ordered the same
                    display_index=int(p.get('Image', i)),
                    archive_index=i,
                    bookmark=p.get('Bookmark', ''),
                    type='',
                )
                md_page.set_type(p.get('Type', ''))

                if isinstance(p.get('DoublePage', None), str):
                    md_page.double_page = p['DoublePage'].casefold() in ('yes', 'true', '1')
                if p.get('ImageHeight', '').isnumeric():
                    md_page.height = int(float(p['ImageHeight']))
                if p.get('ImageWidth', '').isnumeric():
                    md_page.width = int(float(p['ImageWidth']))
                if p.get('ImageSize', '').isnumeric():
                    md_page.byte_size = int(float(p['ImageSize']))

                md.pages.append(md_page)

        md.is_empty = False

        return md

    def _validate_bytes(self, string: bytes) -> bool:
        """Verify that the string actually contains CIX data in XML format."""
        try:
            root = ET.fromstring(string)
            if root.tag != 'ComicInfo':
                return False
        except ET.ParseError:
            return False

        return True
