#!/usr/bin/env python3
#
#  Copyright (C) 2017 Codethink Limited
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2 of the License, or (at your option) any later version.
#
#  This library is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with this library. If not, see <http://www.gnu.org/licenses/>.
#
#  Authors:
#        Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>
import click
from blessings import Terminal

# Import a widget internal for formatting time codes
from .widget import TimeCode


# Status()
#
# A widget for formatting overall status.
#
# Note that the render() and clear() methods in this class are
# simply noops in the case that the application is not connected
# to a terminal, or if the terminal does not support ANSI escape codes.
#
# Args:
#    content_profile (Profile): Formatting profile for content text
#    format_profile (Profile): Formatting profile for formatting text
#    pipeline (Pipeline): The Pipeline
#    scheduler (Scheduler): The Scheduler
#    colors (bool): Whether to print the ANSI color codes in the output
#
class Status():

    def __init__(self, content_profile, format_profile,
                 success_profile, error_profile,
                 pipeline, scheduler, colors=False):

        self.content_profile = content_profile
        self.format_profile = format_profile
        self.success_profile = success_profile
        self.error_profile = error_profile
        self.pipeline = pipeline
        self.scheduler = scheduler
        self.jobs = []
        self.last_lines = 0  # Number of status lines we last printed to console
        self.term = Terminal()
        self.spacing = 1
        self.colors = colors
        self.header = StatusHeader(content_profile, format_profile,
                                   success_profile, error_profile,
                                   pipeline, scheduler)

        self.term_width, _ = click.get_terminal_size()
        self.alloc_lines = 0
        self.alloc_columns = None
        self.line_length = 0
        self.need_alloc = True

    # add_job()
    #
    # Adds a job to track in the status area
    #
    # Args:
    #    element (Element): The element of the job to track
    #    action_name (str): The action name for this job
    #
    def add_job(self, element, action_name):
        elapsed = self.scheduler.elapsed_time()
        job = StatusJob(element, action_name, self.content_profile, self.format_profile, elapsed)
        self.jobs.append(job)
        self.need_alloc = True

    # remove_job()
    #
    # Removes a job currently being tracked in the status area
    #
    # Args:
    #    element (Element): The element of the job to track
    #    action_name (str): The action name for this job
    #
    def remove_job(self, element, action_name):
        self.jobs = [
            job for job in self.jobs
            if not (job.element is element and
                    job.action_name == action_name)
        ]
        self.need_alloc = True

    # clear()
    #
    # Clear the status area, it is necessary to call
    # this before printing anything to the console if
    # a status area is in use.
    #
    # To print some logging to the output and then restore
    # the status, use the following:
    #
    #   status.clear()
    #   ... print something to console ...
    #   status.render()
    #
    def clear(self):

        if not self.term.does_styling:
            return

        for i in range(self.last_lines):
            self.move_up()
            self.clear_line()
        self.last_lines = 0

    # render()
    #
    # Render the status area.
    #
    # If you are not printing a line in addition to rendering
    # the status area, for instance in a timeout, then it is
    # not necessary to call clear().
    def render(self):

        if not self.term.does_styling:
            return

        elapsed = self.scheduler.elapsed_time()

        self.clear()
        self.check_term_width()
        self.allocate()

        # Nothing to render, early return
        if self.alloc_lines == 0:
            return

        # Before rendering the actual lines, we need to add some line
        # feeds for the amount of lines we intend to print first, and
        # move cursor position back to the first line
        for _ in range(self.alloc_lines + self.header.lines):
            click.echo('', err=True)
        for _ in range(self.alloc_lines + self.header.lines):
            self.move_up()

        # Render the one line header
        text = self.header.render(self.term_width, elapsed)
        click.echo(text, color=self.colors, err=True)

        # Now we have the number of columns, and an allocation for
        # alignment of each column
        n_columns = len(self.alloc_columns)
        for line in self.job_lines(n_columns):
            text = ''
            for job in line:
                column = line.index(job)
                text += job.render(self.alloc_columns[column] - job.size, elapsed)

                # Add spacing between columns
                if column < (n_columns - 1):
                    text += ' ' * self.spacing

            # Print the line
            click.echo(text, color=self.colors, err=True)

        # Track what we printed last, for the next clear
        self.last_lines = self.alloc_lines + self.header.lines

    ###########################################
    #         Status area internals           #
    ###########################################
    def check_term_width(self):
        term_width, _ = click.get_terminal_size()
        if self.term_width != term_width:
            self.term_width = term_width
            self.need_alloc = True

    def move_up(self):
        # Explicitly move to beginning of line, fixes things up
        # when there was a ^C or ^Z printed to the terminal.
        click.echo(self.term.move_x(0) + self.term.move_up, nl=False, err=True)

    def clear_line(self):
        click.echo(self.term.clear_eol, nl=False, err=True)

    def allocate(self):
        if not self.need_alloc:
            return

        # State when there is no jobs to display
        alloc_lines = 0
        alloc_columns = []
        line_length = 0

        # Test for the widest width which fits columnized jobs
        for columns in reversed(range(len(self.jobs))):
            alloc_lines, alloc_columns = self.allocate_columns(columns + 1)

            # If the sum of column widths with spacing in between
            # fits into the terminal width, this is a good allocation.
            line_length = sum(alloc_columns) + (columns * self.spacing)
            if line_length < self.term_width:
                break

        self.alloc_lines = alloc_lines
        self.alloc_columns = alloc_columns
        self.line_length = line_length
        self.need_alloc = False

    def job_lines(self, columns):
        for i in range(0, len(self.jobs), columns):
            yield self.jobs[i:i + columns]

    # Returns an array of integers representing the maximum
    # length in characters for each column, given the current
    # list of jobs to render.
    #
    def allocate_columns(self, columns):
        column_widths = [0 for _ in range(columns)]
        lines = 0
        for line in self.job_lines(columns):
            line_len = len(line)
            lines += 1
            for col in range(columns):
                if col < line_len:
                    job = line[col]
                    column_widths[col] = max(column_widths[col], job.size)

        return lines, column_widths


# The header widget renders total elapsed time and the main invocation information
class StatusHeader():

    def __init__(self, content_profile, format_profile,
                 success_profile, error_profile,
                 pipeline, scheduler):

        self.content_profile = content_profile
        self.format_profile = format_profile
        self.success_profile = success_profile
        self.error_profile = error_profile
        self.pipeline = pipeline
        self.scheduler = scheduler
        self.time_code = TimeCode(content_profile, format_profile)
        self.lines = 3

    def render_queue(self, queue):
        processed = str(len(queue.processed_elements))
        skipped = str(len(queue.skipped_elements))
        failed = str(len(queue.failed_elements))

        size = 5  # Space for the formatting '[', ':', ' ', ' ' and ']'
        size += len(queue.complete_name)
        size += len(processed) + len(skipped) + len(failed)
        text = self.format_profile.fmt("(") + \
            self.content_profile.fmt(queue.complete_name) + \
            self.format_profile.fmt(":") + \
            self.success_profile.fmt(processed) + ' ' + \
            self.content_profile.fmt(skipped) + ' ' + \
            self.error_profile.fmt(failed) + \
            self.format_profile.fmt(")")

        return (text, size)

    def render(self, line_length, elapsed):
        line_length = max(line_length, 80)
        size = 0
        text = ''

        session = str(self.pipeline.session_elements)
        total = str(self.pipeline.total_elements)

        # Format and calculate size for pipeline target and overall time code
        size += len(total) + len(session) + 4  # Size for (N/N) with a leading space
        size += 8  # Size of time code
        size += len(self.pipeline.project.name) + 1
        text += self.time_code.render_time(elapsed)
        text += ' ' + self.content_profile.fmt(self.pipeline.project.name)
        text += ' ' + self.format_profile.fmt('(') + \
                self.content_profile.fmt(session) + \
                self.format_profile.fmt('/') + \
                self.content_profile.fmt(total) + \
                self.format_profile.fmt(')')

        line1 = self.centered(text, size, line_length, '=')
        size = 0
        text = ''

        # Format and calculate size for each queue progress
        for queue in self.scheduler.queues:

            # Add spacing
            if self.scheduler.queues.index(queue) > 0:
                size += 2
                text += self.format_profile.fmt('→ ')

            queue_text, queue_size = self.render_queue(queue)
            size += queue_size
            text += queue_text

        line2 = self.centered(text, size, line_length, ' ')

        size = 24
        text = self.format_profile.fmt("~~~~~ ") + \
            self.content_profile.fmt('Active Tasks') + \
            self.format_profile.fmt(" ~~~~~")
        line3 = self.centered(text, size, line_length, ' ')

        return line1 + '\n' + line2 + '\n' + line3

    def centered(self, text, size, line_length, fill):
        remaining = line_length - size
        remaining -= 2

        final_text = self.format_profile.fmt(fill * (remaining // 2)) + ' '
        final_text += text
        final_text += ' ' + self.format_profile.fmt(fill * (remaining // 2))

        return final_text


# A widget for formatting a job in the status area
class StatusJob():

    def __init__(self, element, action_name, content_profile, format_profile, elapsed):
        self.offset = elapsed
        self.element = element
        self.action_name = action_name
        self.content_profile = content_profile
        self.format_profile = format_profile
        self.time_code = TimeCode(content_profile, format_profile)

        # Calculate the size needed to display
        self.size = 10  # Size of time code with brackets
        self.size += len(action_name)
        self.size += len(element.name)
        self.size += 3  # '[' + ':' + ']'

    # render()
    #
    # Render the Job, return a rendered string
    #
    # Args:
    #    padding (int): Amount of padding to print in order to align with columns
    #    elapsed (datetime): The session elapsed time offset
    #
    def render(self, padding, elapsed):
        text = self.format_profile.fmt('[') + \
            self.time_code.render_time(elapsed - self.offset) + \
            self.format_profile.fmt(']')

        # Add padding after the display name, before terminating ']'
        name = self.element.name + (' ' * padding)
        text += self.format_profile.fmt('[') + \
            self.content_profile.fmt(self.action_name) + \
            self.format_profile.fmt(':') + \
            self.content_profile.fmt(name) + \
            self.format_profile.fmt(']')

        return text
