from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

from .config import Config
from .gif_encoder import GIFEncoder
from .opencv_encoder import OpenCVEncoder
from .text_format import irepr
from .text_painter import TextPainter
from .webp_encoder import WebPEncoder

WATERMARK = "Generated by vardbg"
SAMPLE_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ "


class FrameRenderer:
    RED = 0
    GREEN = 1
    BLUE = 2

    def __init__(self, path, config_path):
        # Config
        self.cfg = Config(config_path)
        # Video encoder
        ext = Path(path).suffix.lower()[1:]
        if ext == "mp4":
            self.encoder = OpenCVEncoder(path, "mp4v", self.cfg.fps, self.cfg.w, self.cfg.h)
        elif ext == "gif":
            self.encoder = GIFEncoder(path, self.cfg.fps)
        elif ext == "webp":
            self.encoder = WebPEncoder(path, self.cfg.fps)
        else:
            raise ValueError(f"Unrecognized file extension '{ext}'")
        # Drawing context
        self.draw = None
        # Fonts
        self.body_font = ImageFont.truetype(*self.cfg.font_body)
        self.body_bold_font = ImageFont.truetype(*self.cfg.font_body_bold)
        self.caption_font = ImageFont.truetype(*self.cfg.font_caption)
        self.head_font = ImageFont.truetype(*self.cfg.font_heading)
        self.intro_font = ImageFont.truetype(*self.cfg.font_intro)

        # Whether the watermark has been drawn on this frame
        self._watermark_drawn = False

        # Sizes and positions to be calculated later
        # Code body size
        self.line_height = None
        self.body_cols = None
        self._body_rows = None
        self.body_rows = None
        # Output body start position
        self.out_x = None
        self.out_y = None
        # Output body size
        self.out_cols = None
        self.out_rows = None
        # Variable body start positions
        self.vars_x = None
        self.vars_y = None
        self.ovars_x = None
        self.ovars_y = None
        # Variable body size
        self.vars_cols = None
        self.vars_rows = None
        self.ovars_cols = None
        self.ovars_rows = None

        # Per-frame positions
        self.last_var_x = None
        self.last_var_y = None
        self.ref_var_x = None
        self.ref_var_y = None

        # Current video frame (image)
        self.frame = None

        # Text size cache
        self.text_size_cache = {}

        # Prepare base frame
        self.base_frame = None
        self.prepare_base_frame()

        # Write intro (if necessary)
        if self.cfg.intro_text and self.cfg.intro_time:
            self.write_intro()

    def text_size(self, text, factor=10, **kwargs):
        cache_key = (text, kwargs.get("font", None))

        if cache_key in self.text_size_cache:
            return self.text_size_cache[cache_key]
        else:
            # Multiply string and divide by the factor to get a more precise width
            w, h = self.draw.textsize(text * factor, **kwargs)
            w /= factor

            # Save to cache and return
            sizes = (w, h)
            self.text_size_cache[cache_key] = sizes
            return sizes

    def calc_sizes(self):
        # Calculate text sizes
        w = self.text_size(SAMPLE_CHARS, font=self.body_font)[0] / len(SAMPLE_CHARS)
        hw, hh = self.text_size("A", font=self.head_font)
        _, mh = self.text_size("`^Ag", font=self.body_font)
        _, ch = self.text_size("1p", font=self.caption_font)

        # Code body size
        self.line_height = mh * self.cfg.line_height
        self.body_cols = int((self.cfg.var_x - self.cfg.sect_padding * 2) / w)
        self._body_rows = (self.cfg.out_y - self.cfg.sect_padding * 2 - ch) / self.line_height
        self.body_rows = int(self._body_rows)

        # Output body start position
        self.out_x = self.cfg.sect_padding
        self.out_y = self.cfg.out_y + self.cfg.head_padding * 2 + hh

        # Output body size
        self.out_cols = self.body_cols
        self.out_rows = round((self.cfg.h - self.out_y) / self.line_height)

        # Variable body start positions
        # Top-left X and Y for last variable section
        self.vars_x = self.cfg.var_x + self.cfg.sect_padding
        self.vars_y = self.cfg.head_padding * 2 + hh

        # Columns and rows for last variable section
        self.vars_cols = int((self.cfg.w - self.cfg.var_x - self.cfg.sect_padding * 2) / w)
        self.vars_rows = int((self.cfg.ovar_y - self.cfg.head_padding * 2 - hh) / self.line_height)

        # Top-left X and Y for other variables section
        self.ovars_x = self.vars_x
        self.ovars_y = self.cfg.ovar_y + self.vars_y

        # Columns and rows for other variables section
        self.ovars_cols = self.vars_cols
        ovars_h = self.cfg.h - self.cfg.ovar_y
        self.ovars_rows = int((ovars_h - self.cfg.sect_padding * 2) / self.line_height)

    def get_color(self, col):
        if col == self.RED:
            return self.cfg.red
        elif col == self.GREEN:
            return self.cfg.green
        else:
            return self.cfg.blue

    def draw_text_center(self, x, y, text, font, color):
        w, h = self.text_size(text, font=font)
        self.draw.text((x - w / 2, y - h / 2), text, font=font, fill=color)

    def prepare_base_frame(self):
        # Create new empty frame
        self.new_frame(from_base=False)

        # Draw output section
        # Horizontal divider at 4/5 height
        self.draw.line(((0, self.cfg.out_y), (self.cfg.var_x, self.cfg.out_y)), fill=self.cfg.fg_divider, width=1)
        # Label horizontally centered and padded
        out_center_x = self.cfg.var_x / 2
        out_y = self.cfg.out_y + self.cfg.head_padding
        self.draw_text_center(
            out_center_x, out_y, "Output", self.head_font, self.cfg.fg_heading,
        )

        # Draw variable section
        # Vertical divider at 2/3 width
        self.draw.line(((self.cfg.var_x, 0), (self.cfg.var_x, self.cfg.h)), fill=self.cfg.fg_divider, width=1)
        # Label horizontally centered in the variable section and vertically padded
        var_center_x = self.cfg.var_x + ((self.cfg.w - self.cfg.var_x) / 2)
        self.draw_text_center(var_center_x, self.cfg.head_padding, "Last Variable", self.head_font, self.cfg.fg_heading)

        # Draw other variables section
        # Horizontal divider at 1/3 height
        self.draw.line(
            ((self.cfg.var_x, self.cfg.ovar_y), (self.cfg.w, self.cfg.ovar_y)), fill=self.cfg.fg_divider, width=1
        )
        # Label similar to the first, but in the others section instead
        ovar_label_y = self.cfg.ovar_y + self.cfg.head_padding
        self.draw_text_center(var_center_x, ovar_label_y, "Other Variables", self.head_font, self.cfg.fg_heading)

        # Populate sizes and positions
        self.calc_sizes()

        # Save frame as base and reset current frame
        self.base_frame = self.frame
        self.frame = None

    def new_frame(self, from_base=True):
        # Create image
        if from_base:
            self.frame = self.base_frame.copy()
        else:
            self.frame = Image.new("RGB", (self.cfg.w, self.cfg.h), self.cfg.bg)

        # Create drawing context
        self.draw = ImageDraw.Draw(self.frame)
        # Reset watermark drawn flag
        self._watermark_drawn = False

    def start_frame(self):
        self.new_frame()

    def finish_frame(self, var_state):
        # Bail out if there's no frame to finish
        if self.frame is None:
            return

        # Draw variable state (if available)
        if var_state is not None:
            self.draw_variables(var_state)

        if self.cfg.watermark and not self._watermark_drawn:
            self.draw_watermark()
            self._watermark_drawn = True

        self.encoder.write(self.frame)

    def write_intro(self):
        # Render frame
        self.new_frame(from_base=False)
        x = self.cfg.w / 2
        y = self.cfg.h / 2
        self.draw_text_center(x, y, self.cfg.intro_text, self.intro_font, self.cfg.fg_heading)

        # Repeatedly write frame
        frames = round(self.cfg.intro_time * self.cfg.fps)
        for _ in range(frames):
            self.finish_frame(None)

    def draw_code(self, lines, cur_line):
        cur_idx = cur_line - 1

        # Construct list of (line, highlighted) tuples
        hlines = [(line, i == cur_idx) for i, line in enumerate(lines)]

        # Calculate start and end display indexes with an equivalent number of lines on both sides for context
        ctx_side_lines = (self._body_rows - 1) / 2
        start_idx = round(cur_idx - ctx_side_lines)
        end_idx = round(cur_idx + ctx_side_lines)
        # Accommodate for situations where not enough lines are available at the beginning
        if start_idx < 0:
            start_extra = abs(start_idx)
            end_idx += start_extra
            start_idx = 0
        # Slice selected section
        display_lines = hlines[start_idx:end_idx]

        # Construct painter
        x_start = self.cfg.sect_padding
        y_start = self.cfg.sect_padding + self.line_height
        x_end = self.cfg.var_x - self.cfg.sect_padding
        painter = TextPainter(self, x_start, y_start, self.body_cols, self.body_rows, x_end=x_end, show_truncate=False)

        # Render processed lines
        for i, (line, highlighted) in enumerate(display_lines):
            bg_color = self.cfg.highlight if highlighted else None

            for token, text in line:
                painter.write(text, bg_color=bg_color, **self.cfg.styles[token])

    def draw_output(self, lines):
        lines = lines[-self.out_rows :]
        painter = TextPainter(self, self.out_x, self.out_y, self.out_cols, self.out_rows)
        painter.write("\n".join(lines))

    def draw_exec(self, nr_times, cur, avg, total):
        plural = "" if nr_times == 1 else "s"
        text = f"Line executed {nr_times} time{plural} — current time elapsed: {cur}, average: {avg}, total: {total}"

        _, h = self.text_size(text, font=self.caption_font)
        x = self.cfg.sect_padding
        y = self.cfg.out_y - self.cfg.sect_padding - h
        self.draw.text((x, y), text, font=self.caption_font)

    def draw_last_var(self, state):
        painter = TextPainter(self, self.vars_x, self.vars_y, self.vars_cols, self.vars_rows)

        # Draw variable name
        painter.write(state.name + " ")
        # Draw action with color
        self.last_var_x, self.last_var_y = painter.write(state.action + " ", bold=True, color=state.color)
        painter.new_line()

        # Draw remaining text
        painter.write(state.text)

    def draw_other_vars(self, state):
        painter = TextPainter(self, self.ovars_x, self.ovars_y, self.ovars_cols, self.ovars_rows)

        # Draw text
        for idx, (var, values) in enumerate(state.other_history):
            if values.ignored:
                continue

            if idx > 0:
                painter.write("\n\n")

            painter.write(var.name + ":")

            for v_idx, value in enumerate(values):  # sourcery off
                painter.write("\n    \u2022 ")

                # Reference highlighting for latest value and matching variables only
                if var.name == state.ref and v_idx == len(values) - 1:
                    v_pos = irepr(painter, value.value, state.value, bold=True, color=state.color, return_pos="H")
                    self.ref_var_x, self.ref_var_y = v_pos
                else:
                    irepr(painter, value.value)

    def draw_var_ref(self, state):
        # Calculate X position to route the line on
        # It should be as short as possible while not obscuring any variables or exceeding the scene width
        right_line_x = min(
            max(self.last_var_x, self.ref_var_x) + self.cfg.sect_padding, self.cfg.w - self.cfg.sect_padding / 2
        )

        sw, sh = self.text_size(" ", font=self.body_font)

        # Draw the polyline
        self.draw.line(
            (
                (self.last_var_x, self.last_var_y),
                (right_line_x, self.last_var_y),
                (right_line_x, self.ref_var_y - sh),
                (self.ref_var_x, self.ref_var_y - sh),
                (self.ref_var_x, self.ref_var_y),
            ),
            fill=state.color,
            width=2,
        )

    def draw_variables(self, state):
        self.draw_other_vars(state)
        self.draw_last_var(state)

        if state.ref is not None:
            self.draw_var_ref(state)

    def draw_watermark(self):
        # Get target bottom-right position
        x = self.cfg.w - self.cfg.sect_padding
        y = self.cfg.h - self.cfg.sect_padding

        # Subtract text size to position it properly
        w, h = self.text_size(WATERMARK, font=self.caption_font)
        x -= w
        y -= h

        # Draw text
        self.draw.text((x, y), WATERMARK, fill=self.cfg.fg_watermark, font=self.caption_font)

    def close(self, var_state):
        # Finish final frame
        self.finish_frame(var_state)
        # Close encoder
        self.encoder.stop()
