"""
Logo formatting. Each formatter is a function f(data, format) that draws
a representation of the logo. The main graphical formatter is eps_formatter. A mapping
'formatters' containing all available formatters . Each formatter returns binary data. The eps
and data formats can decoded to strings, e.g. eps_as_string = eps_data.decode()
"""

import shutil
from subprocess import Popen, PIPE
from math import log
from string import Template
import os

from .utils import resource_string
from .logo import LogoData, LogoFormat
from .color import Color

__all__ = ['pdf_formatter', 'jpeg_formatter', 'svg_formatter', 'png_formatter',
           'png_print_formatter', 'txt_formatter', 'eps_formatter', 'formatters',
           'default_formatter', 'GhostscriptAPI']

std_units = {
    "bits": 1. / log(2),
    "nats": 1.,
    "digits": 1. / log(10),
    "kT": 1.,
    "kJ/mol": 8.314472 * 298.15 / 1000.,
    "kcal/mol": 1.987 * 298.15 / 1000.,
    "probability": None,
}
"""Some text"""


def pdf_formatter(logodata: LogoData, logoformat: LogoFormat) -> bytes:
    """ Generate a logo in PDF format."""
    eps = eps_formatter(logodata, logoformat).decode()
    gs = GhostscriptAPI()
    return gs.convert('pdf', eps, logoformat.logo_width, logoformat.logo_height)


def _bitmap_formatter(logodata: LogoData, logoformat: LogoFormat, device: str) -> bytes:
    eps = eps_formatter(logodata, logoformat).decode()
    gs = GhostscriptAPI()
    return gs.convert(device, eps,
                      logoformat.logo_width, logoformat.logo_height, logoformat.resolution)


def jpeg_formatter(logodata: LogoData, logoformat: LogoFormat) -> bytes:
    """ Generate a logo in JPEG format."""
    return _bitmap_formatter(logodata, logoformat, device="jpeg")


def svg_formatter(logodata: LogoData, logoformat: LogoFormat) -> bytes:
    """ Generate a logo in Scalable Vector Graphics (SVG) format.
    Requires the program 'pdf2svg' be installed.
    """
    pdf = pdf_formatter(logodata, logoformat)

    command = shutil.which('pdf2svg')
    if command is None:
        raise EnvironmentError("Scalable Vector Graphics (SVG) format requires the program"
                               "'pdf2svg'.")  # pragma: no cover

    import tempfile
    fpdfi, fname_pdf = tempfile.mkstemp(suffix=".pdf")
    fsvgi, fname_svg = tempfile.mkstemp(suffix=".svg")
    try:

        fpdf2 = open(fname_pdf, 'w')
        fpdf2.buffer.write(pdf)
        fpdf2.seek(0)

        args = [command, fname_pdf, fname_svg]
        p = Popen(args)
        (out, err) = p.communicate()

        fsvg = open(fname_svg)
        return fsvg.read().encode()
    finally:
        os.remove(fname_svg)
        os.remove(fname_pdf)


def png_formatter(logodata: LogoData, logoformat: LogoFormat) -> bytes:
    """ Generate a logo in PNG format."""
    return _bitmap_formatter(logodata, logoformat, device="png")


def png_print_formatter(logodata: LogoData, logoformat: LogoFormat) -> bytes:
    """ Generate a logo in PNG format with print quality (600 DPI) resolution."""
    logoformat.resolution = 600
    return _bitmap_formatter(logodata, logoformat, device="png")


def txt_formatter(logodata: LogoData, logoformat: LogoFormat) -> bytes:
    """ Create a text representation of the logo data.
    """
    return str(logodata).encode()


def eps_formatter(logodata: LogoData, logoformat: LogoFormat) -> bytes:
    """ Generate a logo in Encapsulated Postscript (EPS)"""
    substitutions = {}
    from_format = [
        "creation_date", "logo_width", "logo_height",
        "lines_per_logo", "line_width", "line_height",
        "line_margin_right", "line_margin_left", "line_margin_bottom",
        "line_margin_top", "title_height", "xaxis_label_height",
        "creator_text", "logo_title", "logo_margin",
        "stroke_width", "tic_length",
        "stacks_per_line", "stack_margin",
        "yaxis_label", "yaxis_tic_interval", "yaxis_minor_tic_interval",
        "xaxis_label", "xaxis_tic_interval", "number_interval",
        "fineprint", "shrink_fraction", "errorbar_fraction",
        "errorbar_width_fraction",
        "errorbar_gray", "small_fontsize", "fontsize",
        "title_fontsize", "number_fontsize", "text_font",
        "logo_font", "title_font",
        "logo_label", "yaxis_scale", "end_type",
        "debug", "show_title", "show_xaxis",
        "show_xaxis_label", "show_yaxis", "show_yaxis_label",
        "show_boxes", "show_errorbars", "show_fineprint",
        "rotate_numbers", "show_ends", "stack_height",
        "stack_width"
    ]

    for sf in from_format:
        substitutions[sf] = getattr(logoformat, sf)

    substitutions["shrink"] = str(logoformat.show_boxes).lower()

    def format_color(color: Color) -> str:    # (no fold)
        return " ".join(("[", str(color.red), str(color.green),
                         str(color.blue), "]"))

    substitutions["default_color"] = format_color(logoformat.default_color)

    data = []

    # Unit conversion. 'None' for probability units
    conv_factor = std_units[logoformat.unit_name]

    data.append("StartLine")

    seq_from = logoformat.logo_start - logoformat.first_index
    seq_to = logoformat.logo_end - logoformat.first_index + 1

    # seq_index : zero based index into sequence data
    # logo_index : User visible coordinate, first_index based
    # stack_index : zero based index of visible stacks
    for seq_index in range(seq_from, seq_to):
        # logo_index = seq_index + logoformat.first_index
        stack_index = seq_index - seq_from

        if stack_index != 0 and (stack_index % logoformat.stacks_per_line) == 0:
            data.append("")
            data.append("EndLine")
            data.append("StartLine")
            data.append("")

        data.append("(%s) StartStack" % logoformat.annotate[seq_index])

        if conv_factor:
            stack_height = logodata.entropy[seq_index] * std_units[logoformat.unit_name]
        else:
            stack_height = 1.0  # probability   # pragma: no cover

        # Sort by frequency. If equal frequency then reverse alphabetic
        # (So sort reverse alphabetic first, then frequencty)
        # TODO: doublecheck this actual works
        s = list(zip(logodata.counts[seq_index], logodata.alphabet))
        s.sort(key=lambda x: x[1])
        s.reverse()
        s.sort(key=lambda x: x[0])

        if not logoformat.reverse_stacks:
            s.reverse()         # pragma: no cover

        C = float(sum(logodata.counts[seq_index]))
        if C > 0.0:
            fraction_width = 1.0
            if logoformat.scale_width:
                fraction_width = logodata.weight[seq_index]
                # print(fraction_width, file=sys.stderr)
            for rank, c in enumerate(s):
                color = logoformat.color_scheme.symbol_color(seq_index, c[1], rank)
                data.append(" %f %f %s (%s) ShowSymbol" % (
                    fraction_width, c[0] * stack_height / C,
                    format_color(color), c[1]))

        # Draw error bar on top of logo. Replaced by DrawErrorbarFirst above.
        if logodata.entropy_interval is not None and conv_factor and C > 0.0:

            low, high = logodata.entropy_interval[seq_index]
            center = logodata.entropy[seq_index]
            low *= conv_factor
            high *= conv_factor
            center *= conv_factor
            if high > logoformat.yaxis_scale:
                high = logoformat.yaxis_scale       # pragma: no cover

            down = (center - low)
            up = (high - center)
            data.append(" %f %f DrawErrorbar" % (down, up))

        data.append("EndStack")
        data.append("")

    data.append("EndLine")
    substitutions["logo_data"] = "\n".join(data)

    # Create and output logo
    template = resource_string(__name__, 'template.eps', __file__).decode()
    logo = Template(template).substitute(substitutions)

    return logo.encode()


formatters = {
    'eps': eps_formatter,
    'pdf': pdf_formatter,
    'png': png_formatter,
    'png_print': png_print_formatter,
    'jpeg': jpeg_formatter,
    'svg': svg_formatter,
    'logodata': txt_formatter,
}
"""Map between output format names and corresponing logo formatter"""


default_formatter = eps_formatter
"""The default logo formatter."""


class GhostscriptAPI(object):
    """Interface to the command line program Ghostscript ('gs')"""

    formats = ('png', 'pdf', 'jpeg')

    def __init__(self, path: os.PathLike = None) -> None:
        """
        Raises:
            EnvironmentError: If cannot find Ghostscript executable on
                path
        """
        command = shutil.which('gs', path=path)
        if command is None:
            command = shutil.which('gswin32c.exe', path=path)   # pragma: no cover
        if command is None:
            raise EnvironmentError("Could not find Ghostscript on path. "
                                   "There should be either a gs executable or a gswin32c.exe on "
                                   "your system's path")    # pragma: no cover

        self.command = command

    def version(self) -> str:
        """Returms: The ghostscript version string"""
        args = [self.command, '--version']
        try:
            p = Popen(args, stdout=PIPE)
            (out, err) = p.communicate()
        except OSError:     # pragma: no cover
            raise RuntimeError("Cannot communicate with ghostscript.")  # pragma: no cover
        return out.strip()

    def convert(self,
                format: str,
                postscript: str,
                width: int,
                height: int,
                resolution: int = 300) -> bytes:
        """Convert a string of postscript into a different graphical format

        Supported foramts are 'png', 'pdf', and 'jpeg'.

        Raises:
            ValueError: For an unregonized format.
        """
        device_map = {'png': 'png16m', 'pdf': 'pdfwrite', 'jpeg': 'jpeg'}

        try:
            device = device_map[format]
        except KeyError:                                # pragma: no cover
            raise ValueError("Unsupported format.")

        args = [self.command,
                "-sDEVICE=%s" % device,
                "-dPDFSETTINGS=/printer",
                # "-q",   # Quite: Do not dump messages to stdout.
                "-sstdout=%stderr",  # Redirect messages and errors to stderr
                # fix issue 36, problems with ghostscript 9.10
                "-dColorConversionStrategy=/LeaveColorUnchanged",
                "-sOutputFile=-",  # Stdout
                "-dDEVICEWIDTHPOINTS=%s" % str(width),
                "-dDEVICEHEIGHTPOINTS=%s" % str(height),
                "-dSAFER",  # For added security
                "-dNOPAUSE", ]

        if device != 'pdf':
            args.append("-r%s" % str(resolution))
            if resolution < 300:  # Antialias if resolution is Less than 300 DPI
                args.append("-dGraphicsAlphaBits=4")
                args.append("-dTextAlphaBits=4")
                args.append("-dAlignToPixels=0")

        args.append("-")  # Read from stdin. Must be last argument.

        error_msg = "Unrecoverable error : Ghostscript conversion failed " \
                    "(Invalid postscript?). %s" % " ".join(args)

        try:
            p = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
            (out, err) = p.communicate(postscript.encode())
        except OSError:                         # pragma: no cover
            raise RuntimeError(error_msg)       # pragma: no cover

        if p.returncode != 0:                   # pragma: no cover
            error_msg += '\nReturn code: %i\n' % p.returncode
            if err is not None:
                error_msg += err
            raise RuntimeError(error_msg)

        return out

# end class Ghostscript
