#!/usr/sbin/python

# This file is part of mkchromecast.
import atexit
import os
import signal
import subprocess
import sys
from typing import List, Optional

HERE = os.path.dirname(os.path.realpath(__file__))
if os.path.exists(os.path.join(HERE, '..', 'mkchromecast')):
    sys.path.insert(0, os.path.join(HERE, '..'))

import mkchromecast
from mkchromecast.version import __version__
from mkchromecast.audio_devices import (inputint, inputdev, outputdev,
                                        outputint)
from mkchromecast import cast
from mkchromecast import colors
from mkchromecast.constants import OpMode
from mkchromecast.pulseaudio import create_sink, get_sink_list, remove_sink
from mkchromecast.utils import terminate, checkmktmp, writePidFile


def maybe_execute_single_action(mkcc: mkchromecast.Mkchromecast):
    """Potentially executes a one-off action, followed by exiting."""

    if mkcc.operation == OpMode.RESET:
        # TODO(xsdg): unify the various entry and cleanup codepaths.
        if mkcc.platform == "Darwin":
            inputint()
            outputint()
        else:
            get_sink_list()
            remove_sink()
        terminate()
        sys.exit(0)

    if mkcc.operation == OpMode.VERSION:
        print("mkchromecast " + "v" + colors.success(__version__))
        sys.exit(0)


# TODO(xsdg): Stop using conditional imports all over the place.
class CastProcess(object):
    """Class to manage cast process"""
    def __init__(self, mkcc: mkchromecast.Mkchromecast):
        print(colors.bold('Mkchromecast ') + 'v' + __version__)
        self.mkcc = mkcc

        # Type declarations
        self.cc: cast.Casting

    def run(self):
        self.cc = cast.Casting(self.mkcc)
        checkmktmp()
        writePidFile()

        atexit.register(self.terminate_app)

        self.check_connection()
        if self.mkcc.operation == OpMode.DISCOVER:
            # TODO(xsdg): Move this to maybe_execute_single_action.
            self.cc.initialize_cast()
            terminate()
        elif self.mkcc.operation == OpMode.AUDIOCAST:
            self.start_audiocast()
        elif self.mkcc.operation == OpMode.SOURCE_URL:
            self.start_source_url()
        elif self.mkcc.operation == OpMode.YOUTUBE:
            self.start_youtube()
        elif self.mkcc.operation == OpMode.TRAY:
            self.start_tray()
        elif self.mkcc.operation in {OpMode.INPUT_FILE, OpMode.SCREENCAST}:
            self.cast_video()
        else:
            raise Exception(
                f'Unsupported or unexpected operation: {self.mkcc.operation}')

    def start_audiocast(self):
        if self.mkcc.platform == "Linux" and self.mkcc.adevice is None:
            print('Creating Pulseaudio Sink...')
            print(colors.warning(
                'Open Pavucontrol and Select the Mkchromecast Sink.'))
            create_sink()

        print(colors.important('Starting Local Streaming Server'))
        if self.mkcc.backend == 'node':
            import mkchromecast.node
            mkchromecast.node.stream_audio()
        else:
            import mkchromecast.audio
            mkchromecast.audio.main()
        print(colors.success('[Done]'))

        self.cc.initialize_cast()
        self.get_devices(self.mkcc.select_device)

        if self.mkcc.platform == "Darwin":
            print('Switching to BlackHole...')
            inputdev()
            outputdev()
            print(colors.success('[Done]'))

        self.cc.play_cast()
        self.block_until_exit()

    def start_source_url(self):
        self.cc.initialize_cast()
        self.get_devices(self.mkcc.select_device)
        self.cc.play_cast()
        self.block_until_exit()

    def start_youtube(self):
        import mkchromecast.audio
        mkchromecast.audio.main()
        self.cc.initialize_cast()
        self.get_devices(self.mkcc.select_device)
        self.cc.play_cast()
        self.block_until_exit()

    def cast_video(self):
        """This method launches video casting"""

        if self.mkcc.platform == 'Linux':
            print('Creating Pulseaudio Sink...')
            print(colors.warning('Open Pavucontrol and Select the '
                  'Mkchromecast Sink.'))
            create_sink()

        print(colors.important('Starting Video Cast Process...'))
        import mkchromecast.video
        mkchromecast.video.main()
        self.cc.initialize_cast()
        self.get_devices(self.mkcc.select_device)
        self.cc.play_cast()
        self.block_until_exit()

    def get_devices(self, select_device: bool, write_to_pickle: bool = True):
        """Get chromecast name, and let user select one from a list if
        select_device flag is True.
        """
        # This is done for the case that -s is passed
        if select_device is True:
            self.cc.select_a_device()
            self.cc.input_device(write_to_pickle=write_to_pickle)
            self.cc.get_devices()
        else:
            self.cc.get_devices()

    def check_connection(self):
        """Check if the computer is connected to a network"""
        if self.cc.ip == '127.0.0.1':        # We verify the local IP.
            print(colors.error('Your Computer is not Connected to Any '
                  'Network'))
            terminate()

    def terminate_app(self):
        """Terminate the app (kill app)"""
        if self.mkcc.debug:
            print(f'terminate_app running in pid {os.getpid()}')

        self.cc.stop_cast()
        if self.mkcc.platform == 'Darwin':
            inputint()
            outputint()
        elif self.mkcc.platform == 'Linux':
            remove_sink()
        terminate()  # Does not return.

    def print_controls_msg(self):
        """Messages shown when controls is True"""
        print('')
        print(colors.important('Controls:'))
        print(colors.important('========='))
        print('')
        print(colors.options(           'Volume Up:') + ' u')
        print(colors.options(         'Volume Down:') + ' d')
        print(colors.options(       'Attach device:') + ' a')

        if self.mkcc.videoarg is True:
            print(colors.options(       'Pause Casting:')+' p')
            print(colors.options(      'Resume Casting:')+' r')
        print(colors.options('Quit the Application:')+' q or Ctrl-C')
        print('')

    def block_until_exit(self) -> None:
        """Method to show controls"""
        try:
            if self.mkcc.control:
                from mkchromecast.getch import getch

                self.print_controls_msg()

                while(True):
                    key = getch()
                    if(key == 'u'):
                        self.cc.volume_up()
                        if self.mkcc.backend == 'ffmpeg':
                            if self.mkcc.debug is True:
                                self.print_controls_msg()
                    elif(key == 'd'):
                        self.cc.volume_down()
                        if self.mkcc.backend == 'ffmpeg':
                            if self.mkcc.debug is True:
                                self.print_controls_msg()
                    elif (key == 'a'):
                        print(self.cc.available_devices)
                        self.get_devices(self.mkcc.select_device,
                                         write_to_pickle=False)
                        self.cc.play_cast()
                    elif(key == 'p'):
                        if self.mkcc.videoarg is True:
                            print('Pausing Casting Process...')
                            action = 'pause'
                            self.backend_handler(action, self.mkcc.backend)
                            if self.mkcc.backend == 'ffmpeg':
                                if self.mkcc.debug is True:
                                    self.print_controls_msg()
                    elif(key == 'r'):
                        if self.mkcc.videoarg is True:
                            print('Resuming Casting Process...')
                            action = 'resume'
                            self.backend_handler(action, self.mkcc.backend)
                            if self.mkcc.backend == 'ffmpeg':
                                if self.mkcc.debug is True:
                                    self.print_controls_msg()
                    elif(key in {'q', '\x03'}):
                        # "q" or ^D
                        raise KeyboardInterrupt

            else:
                if self.mkcc.platform == 'Linux' and self.mkcc.adevice is None:
                    print(colors.warning('Remember to open pavucontrol and select '
                          'the mkchromecast sink.'))
                print('')
                print(colors.error('Ctrl-C to kill the Application at any Time'))
                print('')
                signal.pause()

        except KeyboardInterrupt:
            print()
            print(colors.error('Quitting application...'))
            self.terminate_app()

    def backend_handler(self, action, backend):
        """Methods to handle pause and resume state of backends"""
        # TODO(xsdg): Woah, this isn't right.  We should specifically only
        # send signals to processes that are our children (and we should do so
        # by using the Popen library, and not by shelling out to pkill).
        if action == 'pause' and backend == 'ffmpeg':
            subprocess.call(['pkill', '-STOP', '-f', 'ffmpeg'])
        elif action == 'resume' and backend == 'ffmpeg':
            subprocess.call(['pkill', '-CONT', '-f', 'ffmpeg'])
        elif (action == 'pause' and backend == 'node' and
              self.mkcc.platform == 'Linux'):
            subprocess.call(['pkill', '-STOP', '-f', 'nodejs'])
        elif (action == 'resume' and backend == 'node' and
              self.mkcc.platform == 'Linux'):
            subprocess.call(['pkill', '-CONT', '-f', 'nodejs'])
        elif (action == 'pause' and backend == 'node' and
              self.mkcc.platform == 'Darwin'):
            subprocess.call(['pkill', '-STOP', '-f', 'node'])
        elif (action == 'resume' and backend == 'node' and
              self.mkcc.platform == 'Darwin'):
            subprocess.call(['pkill', '-CONT', '-f', 'node'])

        if action == 'pause':
            self.cc.pause()
        elif action == 'resume':
            self.cc.play()

    def start_tray(self):
        """This method starts the system tray"""
        import mkchromecast.systray
        # TODO(xsdg): checkmktmp and writePidFile are guaranteed to have been
        # called already.
        checkmktmp()
        writePidFile()
        mkchromecast.systray.main()


if __name__ == "__main__":
    mkcc = mkchromecast.Mkchromecast()
    maybe_execute_single_action(mkcc)

    CastProcess(mkcc).run()
