#!/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(SoundforestCommand, self).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):
        db_track = self.db.get_track(track.path)

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

    def update(self, track):
        checksum = self.db.update_track_checksum(track)
        if checksum is not None:
            self.message('{0:24} {1}'.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(ChecksumCommand, self).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(CodecsCommand, self).parse_args(args)

        if args.action == 'list':
            for name, codec in self.db.codec_configuration.items():
                self.message('{0} ({1})'.format(codec, codec.description))
                self.message('Extensions')
                self.message('  {0}'.format(','.join(x.extension for x in codec.extensions)))

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

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

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

                self.message('')


class ConfigCommand(SoundforestCommand):
    def parse_args(self, args):
        args = super(ConfigCommand, self).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 {0}'.format(setting))
                settings.append((key, value))
            args.settings = settings

        return args

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

        if args.action == 'list':
            for setting in self.db.settings:
                self.message('{0:16s} {1}'.format(setting.key, setting.value))

        if args.action == 'set':
            for key, value in args.settings:
                self.db.add_setting(key, value)

        if args.action == 'delete':
            for key in args.settings:
                self.db.delete_setting(key)


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

        if args.action == 'list':
            if args.paths:
                playlists = []
                for path in args.paths:
                    for playlist in self.db.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:
                playlists = self.db.playlists

            for playlist in playlists:
                self.message(playlist)
                for path in playlist.tracks:
                    self.message('  {0}'.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(SyncConfigCommand, self).parse_args(args)

        if args.action == 'list':
            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(SyncCommand, self).parse_args(args)

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

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

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

            script.exit(0)

        if args.directories:
            if len([d for d in args.paths if os.path.isdir(d)]) != 2:
                self.script.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.script.exit(1, 'No such target: {0}'.format(arg))

                self.manager.enqueue(target)

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

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


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

        if args.action == 'list':
            if args.tree:
                trees = [self.db.get_tree(args.tree)]
            else:
                trees = self.db.trees

            for tree in trees:
                for path in args.paths:
                    for track in tree.filter_tracks(self.db.session, path):
                        for entry in track.tags:
                            self.message('  {0} = {1}'.format(entry.tag, entry.value))


class PrefixCommand(SoundforestCommand):
    def run(self, args):
        args = super(PrefixCommand, self).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:
                self.db.add_prefix(path)

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

        if args.action == 'list':
            for prefix in self.db.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(TracksCommand, self).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('{0} {1}'.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('  {0}={1}'.format(tag.tag, tag.value))


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

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

        if args.action == 'add':
            for path in args.paths:
                self.db.add_tree(path, tree_type=args.tree_type)

        if args.action == 'delete':
            for path in args.paths:
                self.db.delete_tree(path)

        if args.action == 'update':
            for tree in self.db.trees:
                if args.paths and tree.path not in args.paths:
                    continue
                self.db.update_tree(Tree(tree.path))

        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(TreeTypesCommand, self).parse_args(args)

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

        if args.action == 'add':
            for treetype in args.types:
                self.db.add_tree_type(treetype)

        if args.action == 'delete':
            for treetype in args.types:
                self.db.delete_tree_type(treetype)


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

    def parse_args(self, args):
        args = super(TestCommand, self).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('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()

