import tqdm

import urllib.request
import subprocess
import sys

from spotdl.encode.encoders import EncoderFFmpeg
from spotdl.metadata.embedders import EmbedderDefault
from spotdl.metadata import BadMediaFileError

import spotdl.util

import logging
logger = logging.getLogger(__name__)

CHUNK_SIZE = 16 * 1024

class Track:
    """
    This class allows for various operations on provided track
    metadata.

    Parameters
    ----------
    metadata: `dict`
        Track metadata in standardized form.

    cache_albumart: `bool`
        Whether or not to cache albumart data by making network request
        to the given URL. This caching is done as soon as a
        :class:`Track` object is created.

    Examples
    --------
    + Downloading the audio track *"NCS - Spectre"* in opus format from
      YouTube while simultaneously encoding it to an mp3 format:

        >>> from spotdl.metadata_search import MetadataSearch
        >>> provider = MetadataSearch("ncs spectre")
        >>> metadata = provider.on_youtube()
        # The same metadata can also be retrived using `ProviderYouTube`:
        >>> # from spotdl.metadata.providers import ProviderYouTube
        >>> # provider = ProviderYouTube()
        >>> # metadata = provider.from_query("ncs spectre")
        # However, it is recommended to use `MetadataSearch` whenever
        # possible as it provides a higher level API.
        >>>
        >>> from spotdl.track import Track
        >>> track = Track(metadata)
        >>> stream = metadata["streams"].get(
        ...     quality="best",
        ...     preftype="opus",
        ... )
        >>>
        >>> import spotdl.metadata
        >>> filename = spotdl.metadata.format_string(
        ...     "{artist} - {track-name}.{output-ext}",
        ...     metadata,
        ...     output_extension="mp3",
        ... )
        >>>
        >>> filename
        'NoCopyrightSounds - Alan Walker - Spectre [NCS Release].mp3'
        >>> track.download_while_re_encoding(stream, filename)
    """

    def __init__(self, metadata, cache_albumart=False):
        self.metadata = metadata
        self._chunksize = CHUNK_SIZE
        if cache_albumart:
            self._albumart_thread = self._cache_albumart()
        self._cache_albumart = cache_albumart

    def _cache_albumart(self):
        albumart_thread = spotdl.util.ThreadWithReturnValue(
            target=lambda url: urllib.request.urlopen(url).read(),
            args=(self.metadata["album"]["images"][0]["url"],)
        )
        albumart_thread.start()
        return albumart_thread

    def _calculate_total_chunks(self, filesize):
        """
        Determines the total number of chunks.

        Parameters
        ----------
        filesize: `int`
            Total size of file in bytes.

        Returns
        -------
        chunks: `int`
            Total number of chunks based on the file size and chunk
            size.
        """

        chunks = (filesize // self._chunksize) + 1
        return chunks

    def _make_progress_bar(self, iterations):
        """
        Creates a progress bar using :class:`tqdm`.

        Parameters
        ----------
        iterations: `int`
            Number of iterations to be performed.

        Returns
        -------
        progress_bar: :class:`tqdm.std.tqdm`
            An iterator object.
        """

        progress_bar = tqdm.trange(
            iterations,
            unit_scale=(self._chunksize // 1024),
            unit="KiB",
            dynamic_ncols=True,
            bar_format='{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt}KiB '
                '[{elapsed}<{remaining}, {rate_fmt}{postfix}]',
        )
        return progress_bar

    def download_while_re_encoding(self, stream, target_path, target_encoding=None,
                                   encoder=EncoderFFmpeg(must_exist=False), show_progress=True):
        """
        Downloads a stream while simuntaneously encoding it to a
        given target format.

        Parameters
        ----------
        stream: `dict`
            A `dict` containing stream information in the keys:
            `encoding`, `filesize`, `connection`.

        target_path: `str`
            Path to file to write the target stream to.

        target_encoding: `str`, `None`
            Specify a target encoding. If ``None``, the target encoding
            is automatically determined from the ``target_path``.

        encoder: :class:`spotdl.encode.EncoderBase` object
            A :class:`spotdl.encode.EncoderBase` object to use for encoding.

        show_progress: `bool`
            Whether or not to display a progress bar.
        """

        total_chunks = self._calculate_total_chunks(stream["filesize"])
        process = encoder.re_encode_from_stdin(
            stream["encoding"],
            target_path,
            target_encoding=target_encoding
        )
        response = stream["connection"]

        progress_bar = self._make_progress_bar(total_chunks)
        for _ in progress_bar:
            chunk = response.read(self._chunksize)
            process.stdin.write(chunk)

        process.stdin.close()
        process.wait()

    def download(self, stream, target_path, show_progress=True):
        """
        Downloads a stream.

        Parameters
        ----------
        stream: `dict`
            A `dict` containing stream information in the keys:
            `filesize`, `connection`.

        target_path: `str`
            Path to file to write the downloaded stream to.

        show_progress: `bool`
            Whether or not to display a progress bar.
        """

        total_chunks = self._calculate_total_chunks(stream["filesize"])
        progress_bar = self._make_progress_bar(total_chunks)
        response = stream["connection"]

        def writer(response, progress_bar, file_io):
            for _ in progress_bar:
                chunk = response.read(self._chunksize)
                file_io.write(chunk)

        write_to_stdout = target_path == "-"
        if write_to_stdout:
            file_io = sys.stdout.buffer
            writer(response, progress_bar, file_io)
        else:
            with open(target_path, "wb") as file_io:
                writer(response, progress_bar, file_io)

    def re_encode(self, input_path, target_path, target_encoding=None,
                  encoder=EncoderFFmpeg(must_exist=False), show_progress=True):
        """
        Encodes an already downloaded stream.

        Parameters
        ----------
        input_path: `str`
            Path to input file.

        target_path: `str`
            Path to target file.

        target_encoding: `str`
            Encoding to encode the input file to. If ``None``, the
            target encoding is determined from ``target_path``.

        encoder: :class:`spotdl.encode.EncoderBase` object
            A :class:`spotdl.encode.EncoderBase` object to use for encoding.

        show_progress: `bool`
            Whether or not to display a progress bar.
        """
        stream = self.metadata["streams"].getbest()
        total_chunks = self._calculate_total_chunks(stream["filesize"])
        process = encoder.re_encode_from_stdin(
            stream["encoding"],
            target_path,
            target_encoding=target_encoding
        )
        with open(input_path, "rb") as fin:
            for _ in tqdm.trange(total_chunks):
                chunk = fin.read(self._chunksize)
                process.stdin.write(chunk)

        process.stdin.close()
        process.wait()

    def apply_metadata(self, input_path, encoding=None, embedder=EmbedderDefault()):
        """
        Applies metadata on the audio file.

        Parameters
        ----------
        input_path: `str`
            Path to audio file to apply metadata to.

        encoding: `str`
            Encoding of the input audio file. If ``None``, the target
            encoding is determined from ``input_path``.

        embedder: :class:`spotdl.metadata.embedders.EmbedderDefault`
            An object of :class:`spotdl.metadata.embedders.EmbedderDefault`
            which depicts the metadata embedding strategy to use.
        """
        if self._cache_albumart:
            albumart = self._albumart_thread.join()
        else:
            albumart = None

        try:
            embedder.apply_metadata(
                input_path,
                self.metadata,
                cached_albumart=albumart,
                encoding=encoding,
            )
        except BadMediaFileError as e:
            msg = ("{} Such problems should be fixed "
                  "with FFmpeg set as the encoder.").format(e.args[0])
            logger.warning(msg)