#!/usr/bin/env python
# coding=utf-8
"""
Soundforest database manipulation tool
"""

import os
import sys
import re
import shutil
import argparse

from soundforest import SoundforestError, TreeError
from soundforest.cli import Script, ScriptCommand, ScriptError
from soundforest.prefixes import TreePrefixes
from soundforest.sync import SyncManager, SyncError
from soundforest.tree import Tree, Track, Album


class SoundforestCommand(ScriptCommand):
    def parse_args(self, args):
        args = super().parse_args(args)

        if 'paths' in args and args.paths:
            paths = []
            for v in args.paths:
                if v == '-':
                    for line in [x.rstrip() for x in sys.stdin.readlines()]:
                        if line not in paths:
                            paths.append(line)

                else:
                    stripped = v.rstrip()
                    # Root path / gets empty here
                    if stripped == '':
                        stripped = v
                    if stripped not in paths:
                        paths.append(stripped)

            args.paths = paths

        return args


class ChecksumCommand(SoundforestCommand):
    def verify(self, track):
        try:
            db_track = self.db.get_track(track.path)
        except SoundforestError as e:
            self.error(e)
            return

        if db_track is not None:
            status = db_track.checksum == track.checksum and 'OK' or 'NOK'
        else:
            status = 'NOTFOIND'
        self.message('{:8} {}'.format(status, track.path))

    def update(self, track):
        try:
            checksum = self.db.update_track_checksum(track)
        except SoundforestError as e:
            self.error(e)
        if checksum is not None:
            self.message('{:24} {}'.format(checksum, track.path))

    def process(self, action, track):
        if action == 'update':
            self.update(track)
        elif action == 'verify':
            self.verify(track)

    def run(self, args):
        args = super().parse_args(args)

        for path in args.paths:
            realpath = os.path.realpath(path)

            if os.path.isdir(realpath):
                tree = Tree(path)
                for track in tree:
                    self.process(args.action, track)

            elif os.path.isfile(realpath):
                self.process(args.action, Track(path))


class CodecsCommand(SoundforestCommand):
    def run(self, args):
        args = super().parse_args(args)

        if args.action == 'list':
            try:
                configuration = self.db.codec_configuration
            except SoundforestError as e:
                self.exit(1, e)

            for name, codec in configuration.items():
                self.message('{} ({})'.format(codec, codec.description))
                self.message('Extensions')
                self.message('  {}'.format(','.join(x.extension for x in codec.extensions)))

                self.message('Decoders')
                for decoder in codec.decoders:
                    self.message('  {}'.format(decoder.command))

                self.message('Encoders')
                for encoder in codec.encoders:
                    self.message('  {}'.format(encoder.command))

                if codec.testers:
                    self.message('Testers')
                    for tester in codec.testers:
                        self.message('  {}'.format(tester.command))

                self.message('')


class ConfigCommand(SoundforestCommand):
    def parse_args(self, args):
        args = super().parse_args(args)

        if args.action == 'set':
            settings = []
            for setting in args.settings:
                try:
                    key, value = setting.split('=', 1)
                except ValueError:
                    self.exit(1, 'Error parsing setting from {}'.format(setting))
                settings.append((key, value))
            args.settings = settings

        return args

    def run(self, args):
        args = self.parse_args(args)

        if args.action == 'list':
            try:
                for setting in self.db.settings:
                    self.message('{:16s} {}'.format(setting.key, setting.value))
            except SoundforestError as e:
                self.exit(1, e)

        if args.action == 'set':
            try:
                for key, value in args.settings:
                    self.db.add_setting(key, value)
            except SoundforestError as e:
                self.exit(1, e)

        if args.action == 'delete':
            try:
                for key in args.settings:
                    self.db.delete_setting(key)
            except SoundforestError as e:
                self.exit(1, e)


class PlaylistsCommand(SoundforestCommand):
    def run(self, args):
        args = super().parse_args(args)

        if args.action == 'list':
            if args.paths:
                playlists = []
                for path in args.paths:
                    try:
                        playlists = self.db.playlists
                    except SoundforestError as e:
                        self.exit(1, e)
                    for playlist in playlists:
                        names = ( os.path.basename(path), os.path.splitext(os.path.basename(path))[0], )
                        if os.path.dirname(path) == playlist.directory and playlist.name in names:
                            playlists.append(playlist)
            else:
                try:
                    playlists = self.db.playlists
                except SoundforestError as e:
                    self.exit(1, e)

            for playlist in playlists:
                self.message(playlist)
                for path in playlist.tracks:
                    self.message('  {}'.format(path))

        if args.action == 'add':
            for playlist in args.paths:
                try:
                    self.db.add_playlist(playlist)
                except SoundforestError as e:
                    self.message(e)

        if args.action == 'update':
            for playlist in args.paths:
                try:
                    self.db.update_playlist(playlist)
                except SoundforestError as e:
                    self.message(e)

        if args.action == 'delete':
            for playlist in args.paths:
                try:
                    self.db.delete_playlist(playlist)
                except SoundforestError as e:
                    self.message(e)


class SyncConfigCommand(SoundforestCommand):
    def run(self, args):
        args = super().parse_args(args)

        if args.action == 'list':
            try:
                sync_targets = self.db.sync_targets
            except SoundforestError as e:
                self.exit(1, e)

            for s in self.db.sync_targets:
                self.message(s)

        if args.action == 'add':
            try:
                self.db.add_sync_target(args.name, args.type, args.src, args.dst, args.flags)
            except SoundforestError as e:
                self.exit(1, e)

        if args.action == 'delete':
            try:
                self.db.delete_sync_target(args.name)
            except SoundforestError as e:
                self.exit(1, e)


class SyncCommand(SoundforestCommand):

    def run(self, args):
        args = super().parse_args(args)

        self.manager = SyncManager(threads=args.threads, delete=args.delete, debug=args.debug)

        if args.list:
            try:
                sync = self.db.sync
            except SoundforestError as e:
                self.exit(1, e)

            for name, settings in sync.items():
                if args.paths and name not in args.paths:
                    continue

                self.message('{}'.format(name))
                self.message('  Type:        {}'.format(settings['type']))
                self.message('  Source:      {}'.format(settings['src']))
                self.message('  Destination: {}'.format(settings['dst']))
                self.message('  Flags:       {}'.format(settings['flags']))

            script.exit(0)

        if args.directories:
            if len([d for d in args.paths if os.path.isdir(d)]) != 2:
                self.exit(1, 'Directory sync requires two existing directory paths')

            src = Tree(args.paths[0])
            dst = Tree(args.paths[1])
            self.manager.enqueue({
                'type': 'directory',
                'src': src,
                'dst': dst,
                'rename': args.rename,
            })

        elif args.paths:
            for arg in args.paths:
                target = self.manager.parse_target(arg)
                if not target:
                    self.exit(1, 'No such target: {}'.format(arg))

                self.manager.enqueue(target)

        else:
            try:
                targets = self.db.sync_targets
            except SoundforestError as e:
                self.exit(1, e)

            for target in self.db.sync_targets:
                self.manager.enqueue(target.as_dict())

        if len(self.manager):
            self.manager.run()
        else:
            self.exit(1, 'No sync targets found')


class TagsCommand(SoundforestCommand):
    def run(self, args):
        args = super().parse_args(args)

        if args.action == 'list':
            try:
                if args.tree:
                    trees = [self.db.get_tree(args.tree)]
                else:
                    trees = self.db.trees
            except SoundforestError as e:
                self.exit(1, e)

            for tree in trees:
                for path in args.paths:
                    try:
                        filtered_tracks = tree.filter_tracks(self.db.session, path)
                    except SoundforestError as e:
                        self.error(e)
                    for track in filtered_tracks:
                        for entry in track.tags:
                            self.message('  {} = {}'.format(entry.tag, entry.value))


class PrefixCommand(SoundforestCommand):
    def run(self, args):
        args = super().parse_args(args)

        if args.action == 'match':
            prefixes = TreePrefixes()
            for path in args.paths:
                match = prefixes.match(path)
                if match:
                    self.message(match)

        if args.action == 'add':
            for path in args.paths:
                try:
                    self.db.add_prefix(path)
                except SoundforestError as e:
                    self.exit(1, e)

        if args.action == 'delete':
            for path in args.paths:
                try:
                    self.db.delete_prefix(path)
                except SoundforestError as e:
                    self.exit(1, e)

        if args.action == 'list':
            try:
                tree_prefixes = self.db.tree_prefixes
            except SoundforestError as e:
                self.exit(1, e)
            for prefix in tree_prefixes:
                if args.paths and not self.match_prefix(prefix.path, args.paths):
                    continue

                self.message(prefix)


class TracksCommand(SoundforestCommand):
    def run(self, args):
        args = super().parse_args(args)

        tracks = []
        if args.paths:
            for path in args.paths:
                tracks.extend(self.db.find_tracks(path))
        else:
            for tree in self.db.trees:
                tracks.extend(tree.tracks)

        for track in tracks:
            if args.action == 'list':
                if args.checksum:
                    self.message('{} {}'.format(track.checksum, track.relative_path()))
                else:
                    self.message(track.relative_path())

            if args.action == 'tags':
                self.message(track.relative_path())
                for tag in track.tags:
                    self.message('  {}={}'.format(tag.tag, tag.value))


class TreeCommand(SoundforestCommand):
    def run(self, args):
        args = super().parse_args(args)

        if args.tree_type and args.tree_type not in script.db.tree_types:
            self.exit(1, 'Unsupported tree type: {}'.format(args.tree_type))

        if args.action == 'add':
            for path in args.paths:
                try:
                    self.db.add_tree(path, tree_type=args.tree_type)
                except SoundforestError as e:
                    self.error(e)

        if args.action == 'delete':
            for path in args.paths:
                try:
                    self.db.delete_tree(path)
                except SoundforestError as e:
                    self.error(e)

        if args.action == 'update':
            for tree in self.db.trees:
                if args.paths and tree.path not in args.paths:
                    continue
                try:
                    self.db.update_tree(Tree(tree.path), update_checksum=args.checksums)
                except TreeError as e:
                    self.error(e)

        if args.action == 'list':
            for tree in self.db.trees:
                if args.tree_type and tree.type != args.tree_type:
                    continue

                if args.paths and not self.match_path(tree.path, args.paths):
                    continue

                self.message(tree)


class TreeTypesCommand(SoundforestCommand):
    def run(self, args):
        args = super().parse_args(args)

        if args.action == 'list':
            for treetype in self.db.tree_types:
                self.message( '{:14s} {}'.format(treetype.name, treetype.description))

        if args.action == 'add':
            for treetype in args.types:
                try:
                    self.db.add_tree_type(treetype)
                except SoundforestError as e:
                    self.error(e)

        if args.action == 'delete':
            for treetype in args.types:
                try:
                    self.db.delete_tree_type(treetype)
                except SoundforestError as e:
                    self.error(e)


class TestCommand(SoundforestCommand):
    def testresult(self, track, result, errors='', stdout=None, stderr=None):
        if not result:
            self.message( '{} {}{}'.format('NOK', track.path, errors and ': {0}'.format(errors) or ''))

    def parse_args(self, args):
        args = super().parse_args(args)

        if not args.paths:
            self.exit(1, 'No paths to test provided')

        return args

    def run(self, args):
        args = self.parse_args(args)

        errors = False
        for path in args.paths:
            realpath = os.path.realpath(path)
            if os.path.isdir(realpath):
                if Tree(path).test(callback=self.testresult) != 0:
                    errors = True

            elif os.path.isfile(realpath):
                try:
                    if Track(path).test(callback=self.testresult) != 0:
                        errors = True
                except TreeError as e:
                    script.message(e)
                    errors = True

        if errors:
            self.exit(1)

        else:
            self.exit(0)


script = Script()

c = script.add_subcommand(ChecksumCommand('checksum', description='Check and update track checksums'))
c.add_argument('action', choices=('verify', 'update'), help='Checksum action')
c.add_argument('paths', nargs='*', help='Paths to process')

c = script.add_subcommand(CodecsCommand('codec', 'Codec database manipulations'))
c.add_argument('-v', '--verbose', action='store_true', help='Verbose details')
c.add_argument('action', choices=('list',), help='Codec database action')

c = script.add_subcommand(ConfigCommand('config', 'Configuration database manipulations'))
c.add_argument('action', choices=('list', 'set', 'delete',), help='List trees in database')
c.add_argument('-v', '--verbose', action='store_true', help='Verbose details')
c.add_argument('settings', nargs='*', help='Settings to process')

c = script.add_subcommand(PlaylistsCommand('playlist', 'Playlist database manipulations'))
c.add_argument('action', choices=('list', 'add', 'update', 'delete', ), help='Action to perform')
c.add_argument('paths', nargs='*', help='Paths to directories to process')

c = script.add_subcommand(SyncConfigCommand('syncconfig', 'Manage tree sync configurations'))
c.add_argument('action', choices=('list', 'add', 'delete',), help='Action to perform')
c.add_argument('name', nargs='?', help='Sync target name')
c.add_argument('type', choices=('rsync', 'directory',), nargs='?', help='Sync type')
c.add_argument('flags', nargs='?', help='Flags for sync command')
c.add_argument('src', nargs='?', help='Source path')
c.add_argument('dst', nargs='?', help='Destination path')

c = script.add_subcommand(SyncCommand('sync', 'Synchronize files and trees'))
c.add_argument('-d', '--directories', action='store_true', help='Sync directories, not configured targets')
c.add_argument('-l', '--list', action='store_true', help='List configured sync targets')
c.add_argument('-r', '--rename', help='Directory sync target filesystem rename callback')
c.add_argument('-D', '--delete', action='store_true', help='Remove unknown files from target')
c.add_argument('-t', '--threads', type=int, help='Number of sync threads to use')
c.add_argument('paths', metavar='path', nargs='*', help='Paths to process')

c = script.add_subcommand(TagsCommand('tag', 'Track tag database manipulations'))
c.add_argument('-t', '--tree', help='Tree to match')
c.add_argument('action', choices=('list',), help='List trees in database')
c.add_argument('paths', nargs='*', help='Paths to trees to process')

c = script.add_subcommand(TracksCommand('track', 'Tree database manipulations'))
c.add_argument('-c', '--checksum', action='store_true', help='Show track checksum')
c.add_argument('action', choices=('list', 'tags',), help='List tracks in database')
c.add_argument('paths', nargs='*', help='Paths to trees to matches')

c = script.add_subcommand(PrefixCommand('prefix', description='Prefix database manipulations'))
c.add_argument('action', choices=('list', 'match', 'add', 'delete'), help='Prefix database action')
c.add_argument('paths', nargs='*', help='Paths to prefixes to process')

c = script.add_subcommand(TreeTypesCommand('treetype', description='Tree type database manipulations'))
c.add_argument('action', choices=('list', 'add', 'delete'), help='List tree types in database')
c.add_argument('types', nargs='*', help='Tree type names to process')

c = script.add_subcommand(TreeCommand('tree', description='Tree database manipulations'))
c.add_argument('-t', '--tree-type', help='Type of audio files in tree')
c.add_argument('-c', '--checksums', action='store_true', help='Update track checksums')
c.add_argument('action', choices=('list', 'update', 'add', 'delete'), help='Tree database action')
c.add_argument('paths', nargs='*', help='Paths to trees to process')

c = script.add_subcommand(TestCommand('test', 'Test file integrity'))
c.add_argument('paths', nargs='*', help='Paths to test')

script.run()

