# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2019 Laurent Monin
# Copyright (C) 2019-2020 Philipp Wolfer
# Copyright (C) 2019 Zenara Daley
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.


import os.path

import mutagen

from test.picardtestcase import PicardTestCase

from picard import config
from picard.formats import id3
from picard.metadata import Metadata

from .common import (
    CommonTests,
    load_metadata,
    load_raw,
    save_and_load_metadata,
    save_metadata,
    save_raw,
    skipUnlessTestfile,
)
from .coverart import CommonCoverArtTests


# prevent unittest to run tests in those classes
class CommonId3Tests:

    class Id3TestCase(CommonTests.TagFormatsTestCase):

        def setup_tags(self):
            # Note: in ID3v23, the original date can only be stored as a year.
            super().setup_tags()
            self.set_tags({
                'originaldate': '1980'
            })
            self.unsupported_tags['r128_album_gain'] = '-2857'
            self.unsupported_tags['r128_track_gain'] = '-2857'

        @skipUnlessTestfile
        def test_id3_freeform_delete(self):
            metadata = Metadata(self.tags)
            metadata['Foo'] = 'Foo'
            original_metadata = save_and_load_metadata(self.filename, metadata)
            del metadata['Foo']
            new_metadata = save_and_load_metadata(self.filename, metadata)

            self.assertIn('Foo', original_metadata)
            self.assertNotIn('Foo', new_metadata)

        @skipUnlessTestfile
        def test_id3_ufid_delete(self):
            metadata = Metadata(self.tags)
            metadata['musicbrainz_recordingid'] = "Foo"
            original_metadata = save_and_load_metadata(self.filename, metadata)
            del metadata['musicbrainz_recordingid']
            new_metadata = save_and_load_metadata(self.filename, metadata)

            self.assertIn('musicbrainz_recordingid', original_metadata)
            self.assertNotIn('musicbrainz_recordingid', new_metadata)

        @skipUnlessTestfile
        def test_id3_multiple_freeform_delete(self):
            metadata = Metadata(self.tags)
            metadata['Foo'] = 'Foo'
            metadata['Bar'] = 'Foo'
            metadata['FooBar'] = 'Foo'
            original_metadata = save_and_load_metadata(self.filename, metadata)
            del metadata['Foo']
            del metadata['Bar']
            new_metadata = save_and_load_metadata(self.filename, metadata)

            self.assertIn('Foo', original_metadata)
            self.assertIn('Bar', original_metadata)
            self.assertIn('FooBar', original_metadata)
            self.assertNotIn('Foo', new_metadata)
            self.assertNotIn('Bar', new_metadata)
            self.assertIn('FooBar', new_metadata)

        @skipUnlessTestfile
        def test_id3_rename_freetext_delete(self):
            tags = mutagen.id3.ID3Tags()
            tags.add(mutagen.id3.TXXX(desc='Work', text='foo'))
            save_raw(self.filename, tags)
            raw_metadata = load_raw(self.filename)
            self.assertIn('TXXX:Work', raw_metadata)

            metadata = Metadata()
            metadata.delete('work')
            new_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertNotIn('work', new_metadata)
            raw_metadata = load_raw(self.filename)
            self.assertNotIn('TXXX:Work', raw_metadata)
            self.assertNotIn('TXXX:WORK', raw_metadata)

        @skipUnlessTestfile
        def test_id3_freetext_ci_delete(self):
            # No matter which of the names below gets deleted it always
            # should remove all of them
            tag_name_variants = [
                'replaygain_album_gain',
                'REPLAYGAIN_ALBUM_GAIN',
                'Replaygain_Album_Gain',
            ]

            for tag_in_test in tag_name_variants:
                tags = mutagen.id3.ID3Tags()
                for tag in tag_name_variants:
                    tags.add(mutagen.id3.TXXX(desc=tag, text='foo'))
                save_raw(self.filename, tags)
                raw_metadata = load_raw(self.filename)
                for tag in tag_name_variants:
                    self.assertIn('TXXX:' + tag, raw_metadata)

                metadata = Metadata()
                metadata.delete(tag_in_test)
                save_metadata(self.filename, metadata)
                raw_metadata = load_raw(self.filename)
                for tag in tag_name_variants:
                    self.assertNotIn('TXXX:' + tag, raw_metadata)

        @skipUnlessTestfile
        def test_id3_metadata_tofn(self):
            metadata = Metadata(self.tags)
            metadata = save_and_load_metadata(self.filename, metadata)

            self.assertIn('originalfilename', metadata)
            self.assertEqual(metadata['originalfilename'], "Foo")

        @skipUnlessTestfile
        def test_performer_duplication(self):

            config.setting['write_id3v23'] = True
            metadata = Metadata({
                'album': 'Foo',
                'artist': 'Foo',
                'performer:piano': 'Foo',
                'title': 'Foo',
            })
            original_metadata = save_and_load_metadata(self.filename, metadata)
            new_metadata = save_and_load_metadata(self.filename, original_metadata)

            self.assertEqual(len(new_metadata['performer:piano']), len(original_metadata['performer:piano']))

        @skipUnlessTestfile
        def test_comment_delete(self):
            metadata = Metadata(self.tags)
            metadata['comment:bar'] = 'Foo'
            metadata['comment:XXX:withlang'] = 'Foo'
            original_metadata = save_and_load_metadata(self.filename, metadata)
            del metadata['comment:bar']
            del metadata['comment:XXX:withlang']
            new_metadata = save_and_load_metadata(self.filename, metadata)

            self.assertIn('comment:foo', original_metadata)
            self.assertIn('comment:bar', original_metadata)
            self.assertIn('comment:XXX:withlang', original_metadata)
            self.assertIn('comment:foo', new_metadata)
            self.assertNotIn('comment:bar', new_metadata)
            self.assertNotIn('comment:XXX:withlang', new_metadata)

        @skipUnlessTestfile
        def test_id3v23_simple_tags(self):
            config.setting['write_id3v23'] = True
            metadata = Metadata(self.tags)
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            for (key, value) in self.tags.items():
                self.assertEqual(loaded_metadata[key], value, '%s: %r != %r' % (key, loaded_metadata[key], value))

        @property
        def itunes_grouping_metadata(self):
            metadata = Metadata()
            metadata['grouping'] = 'The Grouping'
            metadata['work'] = 'The Work'
            return metadata

        @skipUnlessTestfile
        def test_standard_grouping(self):
            metadata = self.itunes_grouping_metadata

            config.setting['itunes_compatible_grouping'] = False
            loaded_metadata = save_and_load_metadata(self.filename, metadata)

            self.assertEqual(loaded_metadata['grouping'], metadata['grouping'])
            self.assertEqual(loaded_metadata['work'], metadata['work'])

        @skipUnlessTestfile
        def test_itunes_compatible_grouping(self):
            metadata = self.itunes_grouping_metadata

            config.setting['itunes_compatible_grouping'] = True
            loaded_metadata = save_and_load_metadata(self.filename, metadata)

            self.assertEqual(loaded_metadata['grouping'], metadata['grouping'])
            self.assertEqual(loaded_metadata['work'], metadata['work'])

        @skipUnlessTestfile
        def test_always_read_grp1(self):
            metadata = self.itunes_grouping_metadata

            config.setting['itunes_compatible_grouping'] = True
            save_metadata(self.filename, metadata)
            config.setting['itunes_compatible_grouping'] = False
            loaded_metadata = load_metadata(self.filename)

            self.assertIn(metadata['grouping'], loaded_metadata['grouping'])
            self.assertIn(metadata['work'], loaded_metadata['grouping'])
            self.assertEqual(loaded_metadata['work'], '')

        @skipUnlessTestfile
        def test_always_read_txxx_work(self):
            metadata = self.itunes_grouping_metadata

            config.setting['itunes_compatible_grouping'] = False
            save_metadata(self.filename, metadata)
            config.setting['itunes_compatible_grouping'] = True
            loaded_metadata = load_metadata(self.filename)

            self.assertIn(metadata['grouping'], loaded_metadata['work'])
            self.assertIn(metadata['work'], loaded_metadata['work'])
            self.assertEqual(loaded_metadata['grouping'], '')

        @skipUnlessTestfile
        def test_save_itunnorm_tag(self):
            config.setting['clear_existing_tags'] = True
            iTunNORM = '00001E86 00001E86 0000A2A3 0000A2A3 000006A6 000006A6 000078FA 000078FA 00000211 00000211'
            metadata = Metadata()
            metadata['comment:iTunNORM'] = iTunNORM
            new_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertEqual(new_metadata['comment:iTunNORM'], iTunNORM)

        @skipUnlessTestfile
        def test_delete_itun_tags(self):
            metadata = Metadata()
            metadata['comment:iTunNORM'] = '00001E86 00001E86 0000A2A3 0000A2A3 000006A6 000006A6 000078FA 000078FA 00000211 00000211'
            metadata['comment:iTunPGAP'] = '1'
            new_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertIn('comment:iTunNORM', new_metadata)
            self.assertIn('comment:iTunPGAP', new_metadata)
            del metadata['comment:iTunNORM']
            del metadata['comment:iTunPGAP']
            new_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertNotIn('comment:iTunNORM', new_metadata)
            self.assertNotIn('comment:iTunPGAP', new_metadata)

        def test_rename_txxx_tags(self):
            file_path = os.path.join('test', 'data', 'test-id3-rename-tags.mp3')
            filename = self.copy_file_tmp(file_path, 'mp3')
            raw_metadata = load_raw(filename)
            self.assertIn('TXXX:Artists', raw_metadata)
            self.assertNotIn('TXXX:ARTISTS', raw_metadata)
            self.assertIn('TXXX:Work', raw_metadata)
            self.assertNotIn('TXXX:WORK', raw_metadata)
            metadata = load_metadata(filename)
            self.assertEqual(metadata['artists'], 'Artist1; Artist2')
            self.assertNotIn('Artists', metadata)
            self.assertEqual(metadata['work'], 'The Work')
            self.assertNotIn('Work', metadata)
            save_metadata(filename, metadata)
            raw_metadata = load_raw(filename)
            self.assertNotIn('TXXX:Artists', raw_metadata)
            self.assertIn('TXXX:ARTISTS', raw_metadata)
            self.assertNotIn('TXXX:Work', raw_metadata)
            self.assertIn('TXXX:WORK', raw_metadata)

        def test_preserve_unchanged_tags_v23(self):
            config.setting['write_id3v23'] = True
            self.test_preserve_unchanged_tags()

        @skipUnlessTestfile
        def test_replaygain_tags_case_insensitive(self):
            tags = mutagen.id3.ID3Tags()
            tags.add(mutagen.id3.TXXX(desc='replaygain_album_gain', text='-6.48 dB'))
            tags.add(mutagen.id3.TXXX(desc='Replaygain_Album_Peak', text='0.978475'))
            tags.add(mutagen.id3.TXXX(desc='replaygain_album_range', text='7.84 dB'))
            tags.add(mutagen.id3.TXXX(desc='replaygain_track_gain', text='-6.16 dB'))
            tags.add(mutagen.id3.TXXX(desc='REPLAYGAIN_track_peak', text='0.976991'))
            tags.add(mutagen.id3.TXXX(desc='REPLAYGAIN_TRACK_RANGE', text='8.22 dB'))
            tags.add(mutagen.id3.TXXX(desc='replaygain_reference_loudness', text='-18.00 LUFS'))
            save_raw(self.filename, tags)
            loaded_metadata = load_metadata(self.filename)
            for (key, value) in self.replaygain_tags.items():
                self.assertEqual(loaded_metadata[key], value, '%s: %r != %r' % (key, loaded_metadata[key], value))

        @skipUnlessTestfile
        def test_ci_tags_save(self):
            tag_name_variants = [
                'replaygain_album_gain',
                'REPLAYGAIN_ALBUM_GAIN',
                'Replaygain_Album_Gain',
            ]

            for tag in tag_name_variants:
                metadata = Metadata({ tag: 'foo' })
                loaded_metadata = save_and_load_metadata(self.filename, metadata)
                self.assertEqual('foo', loaded_metadata['replaygain_album_gain'])

        @skipUnlessTestfile
        def test_ci_tags_preserve_case(self):
            # Ensure values are not duplicated on repeated save and are saved
            # case preserving.
            tags = mutagen.id3.ID3Tags()
            tags.add(mutagen.id3.TXXX(desc='Replaygain_Album_Peak', text='0.978475'))
            save_raw(self.filename, tags)
            loaded_metadata = load_metadata(self.filename)
            loaded_metadata['replaygain_album_peak'] = '1.0'
            save_metadata(self.filename, loaded_metadata)
            raw_metadata = load_raw(self.filename)
            self.assertIn('TXXX:Replaygain_Album_Peak', raw_metadata)
            self.assertEqual(
                raw_metadata['TXXX:Replaygain_Album_Peak'].text[0],
                loaded_metadata['replaygain_album_peak'])
            self.assertEqual(1, len(raw_metadata['TXXX:Replaygain_Album_Peak'].text))
            self.assertNotIn('TXXX:REPLAYGAIN_ALBUM_PEAK', raw_metadata)

        @skipUnlessTestfile
        def test_lyrics_with_description(self):
            metadata = Metadata({'lyrics:foo': 'bar'})
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertEqual(metadata['lyrics:foo'], loaded_metadata['lyrics:foo'])

        @skipUnlessTestfile
        def test_invalid_track_and_discnumber(self):
            metadata = Metadata({
                'discnumber': 'notanumber',
                'tracknumber': 'notanumber',
            })
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertNotIn('discnumber', loaded_metadata)
            self.assertNotIn('tracknumber', loaded_metadata)

        @skipUnlessTestfile
        def test_save_explicit_id3_frames(self):
            metadata = Metadata({
                '~id3:TXXX:foo': 'bar',
                '~id3:TOWN': 'owner'
            })
            save_metadata(self.filename, metadata)
            raw_metadata = load_raw(self.filename)
            self.assertIn('TXXX:foo', raw_metadata)
            self.assertEqual('bar', raw_metadata['TXXX:foo'])
            self.assertEqual('owner', raw_metadata['TOWN'])

        @skipUnlessTestfile
        def test_delete_explicit_id3_frames(self):
            tags = mutagen.id3.ID3Tags()
            tags.add(mutagen.id3.TOWN(text='bar'))
            tags.add(mutagen.id3.TXXX(desc='foo', text='bar1'))
            tags.add(mutagen.id3.TXXX(desc='foo', text='bar2'))
            save_raw(self.filename, tags)
            raw_metadata = load_raw(self.filename)
            self.assertIn('TOWN', raw_metadata)
            self.assertIn('TXXX:foo', raw_metadata)
            metadata = Metadata()
            metadata.delete('~id3:TOWN')
            metadata.delete('~id3:TXXX:foo')
            metadata.delete('~id3:NOTAFRAME')
            save_metadata(self.filename, metadata)
            raw_metadata = load_raw(self.filename)
            self.assertNotIn('TOWN', raw_metadata)
            self.assertNotIn('TXXX:foo', raw_metadata)

        @skipUnlessTestfile
        def test_delete_tipl(self):
            tags = mutagen.id3.ID3Tags()
            tags.add(mutagen.id3.TIPL(people=[
                ['mix', 'mixer1'],
                ['mix', 'mixer2'],
                ['producer', 'producer1'],
            ]))
            save_raw(self.filename, tags)
            metadata = Metadata()
            metadata.delete('mixer')
            save_metadata(self.filename, metadata)
            raw_metadata = load_raw(self.filename)
            people = raw_metadata['TIPL'].people
            self.assertIn(['producer', 'producer1'], people)
            self.assertNotIn(['mix', 'mixer1'], people)
            self.assertNotIn(['mix', 'mixer2'], people)
            self.assertEqual(1, len(people))

        @skipUnlessTestfile
        def test_load_conflicting_txxx_tags(self):
            tags = mutagen.id3.ID3Tags()
            tags.add(mutagen.id3.TXXX(desc='title', text='foo'))
            save_raw(self.filename, tags)
            loaded_metadata = load_metadata(self.filename)
            self.assertEqual('foo', loaded_metadata['~id3:TXXX:title'])

        @skipUnlessTestfile
        def test_license_single_url(self):
            metadata = Metadata({
                'license': 'http://example.com'
            })
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertEqual(metadata['license'], loaded_metadata['license'])
            raw_metadata = load_raw(self.filename)
            self.assertEqual(metadata['license'], raw_metadata['WCOP'])

        @skipUnlessTestfile
        def test_license_single_non_url(self):
            metadata = Metadata({
                'license': 'foo'
            })
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertEqual(metadata['license'], loaded_metadata['license'])
            raw_metadata = load_raw(self.filename)
            self.assertEqual(metadata['license'], raw_metadata['TXXX:LICENSE'])

        @skipUnlessTestfile
        def test_license_multi_url(self):
            metadata = Metadata({
                'license': [
                    'http://example.com/1',
                    'http://example.com/2',
                ]
            })
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertEqual(metadata['license'], loaded_metadata['license'])
            raw_metadata = load_raw(self.filename)
            self.assertEqual(
                set(metadata.getall('license')),
                set(raw_metadata.get('TXXX:LICENSE').text))

        @skipUnlessTestfile
        def test_license_wcop_and_txxx(self):
            tags = mutagen.id3.ID3Tags()
            tags.add(mutagen.id3.WCOP(url='http://example.com/1'))
            tags.add(mutagen.id3.TXXX(desc='license', text='http://example.com/2'))
            save_raw(self.filename, tags)
            loaded_metadata = load_metadata(self.filename)
            loaded_licenses = loaded_metadata.getall('license')
            self.assertEqual(2, len(loaded_licenses))
            self.assertIn('http://example.com/1', loaded_licenses)
            self.assertIn('http://example.com/2', loaded_licenses)


class MP3Test(CommonId3Tests.Id3TestCase):
    testfile = 'test.mp3'
    supports_ratings = True
    expected_info = {
        'length': 156,
        '~channels': '2',
        '~sample_rate': '44100',
    }
    unexpected_info = ['~video']

    @skipUnlessTestfile
    def test_remove_apev2(self):
        # Add APEv2 tags
        apev2_tags = mutagen.apev2.APEv2()
        apev2_tags['Title'] = 'foo'
        apev2_tags.save(self.filename)
        self.assertEqual('foo', mutagen.apev2.APEv2(self.filename)['Title'])
        config.setting['remove_ape_from_mp3'] = False
        save_metadata(self.filename, Metadata())
        self.assertEqual('foo', mutagen.apev2.APEv2(self.filename)['Title'])
        config.setting['remove_ape_from_mp3'] = True
        save_metadata(self.filename, Metadata())
        self.assertRaises(mutagen.apev2.APENoHeaderError, mutagen.apev2.APEv2, self.filename)

    @skipUnlessTestfile
    def test_remove_apev2_no_existing_tags(self):
        self.assertRaises(mutagen.apev2.APENoHeaderError, mutagen.apev2.APEv2, self.filename)
        config.setting['remove_ape_from_mp3'] = True
        save_metadata(self.filename, Metadata())
        self.assertRaises(mutagen.apev2.APENoHeaderError, mutagen.apev2.APEv2, self.filename)


class TTATest(CommonId3Tests.Id3TestCase):
    testfile = 'test.tta'
    supports_ratings = True
    expected_info = {
        'length': 82,
        '~sample_rate': '44100',
    }
    unexpected_info = ['~video']


class DSFTest(CommonId3Tests.Id3TestCase):
    testfile = 'test.dsf'
    supports_ratings = True
    expected_info = {
        'length': 10,
        '~channels': '2',
        '~sample_rate': '5644800',
        '~bitrate': '11289.6',
        '~bits_per_sample': '1',
    }
    unexpected_info = ['~video']


class AIFFTest(CommonId3Tests.Id3TestCase):
    testfile = 'test.aiff'
    supports_ratings = False
    expected_info = {
        'length': 82,
        '~channels': '2',
        '~sample_rate': '44100',
        '~bitrate': '1411.2',
    }
    unexpected_info = ['~video']


class Id3UtilTest(PicardTestCase):
    def test_id3text(self):
        teststring = '日本語testÖäß'
        self.assertEqual(id3.id3text(teststring, 0), '???testÖäß')
        self.assertEqual(id3.id3text(teststring, 1), teststring)
        self.assertEqual(id3.id3text(teststring, 3), teststring)

    def test_image_type_from_id3_num(self):
        self.assertEqual(id3.image_type_from_id3_num(0), 'other')
        self.assertEqual(id3.image_type_from_id3_num(3), 'front')
        self.assertEqual(id3.image_type_from_id3_num(6), 'medium')
        self.assertEqual(id3.image_type_from_id3_num(9999), 'other')

    def test_image_type_as_id3_num(self):
        self.assertEqual(id3.image_type_as_id3_num('other'), 0)
        self.assertEqual(id3.image_type_as_id3_num('front'), 3)
        self.assertEqual(id3.image_type_as_id3_num('medium'), 6)
        self.assertEqual(id3.image_type_as_id3_num('track'), 6)
        self.assertEqual(id3.image_type_as_id3_num('unknowntype'), 0)

    def test_types_from_id3(self):
        self.assertEqual(id3.types_from_id3(0), ['other'])
        self.assertEqual(id3.types_from_id3(3), ['front'])
        self.assertEqual(id3.types_from_id3(6), ['medium'])
        self.assertEqual(id3.types_from_id3(9999), ['other'])


class Mp3CoverArtTest(CommonCoverArtTests.CoverArtTestCase):
    testfile = 'test.mp3'
