#!/usr/bin/env python3

import re
import sys

import cairo
import gtk


## DataRange class
class DataRange:
    ## @var start
    #  start
    ## @var end
    #  end
    ## @var value
    #  value
    def __init__(self, start=0, end=0, value=""):
        """! Initializer
        @param self this object
        @param start start
        @param end end
        @param value value
        """
        self.start = start
        self.end = end
        self.value = value


## EventString class
class EventString:
    ## @var at
    #  at
    ## @var value
    #  value
    def __init__(self, at=0, value=""):
        """! Initializer
        @param self this object
        @param at you
        @param value value
        """
        self.at = at
        self.value = value


## EventFloat class
class EventFloat:
    ## @var at
    #  at
    ## @var value
    #  value
    def __init__(self, at=0, value=0.0):
        """! Initializer
        @param self this object
        @param at you
        @param value value
        """
        self.at = at
        self.value = value


## EventInt class
class EventInt:
    ## @var at
    #  at
    ## @var value
    #  value
    def __init__(self, at=0, value=0.0):
        """! Initializer
        @param self this object
        @param at you
        @param value value
        """
        self.at = at
        self.value = value


def ranges_cmp(a, b):
    diff = a.start - b.start
    if diff < 0:
        return -1
    elif diff > 0:
        return +1
    else:
        return 0


def events_cmp(a, b):
    diff = a.at - b.at
    if diff < 0:
        return -1
    elif diff > 0:
        return +1
    else:
        return 0


## TimelineDataRange
class TimelineDataRange:
    ## @var name
    #  name
    ## @var ranges
    #  ranges
    def __init__(self, name=""):
        """! Initializer
        @param self this object
        @param name name
        """
        self.name = name
        self.ranges = []
        return

    def __search(self, key):
        """! Search
        @param self this object
        @param key key
        @return index if found or -1 if not found
        """
        l = 0
        u = len(self.ranges) - 1
        while l <= u:
            i = int((l + u) / 2)
            if key >= self.ranges[i].start and key <= self.ranges[i].end:
                return i
            elif key < self.ranges[i].start:
                u = i - 1
            else:
                # key > self.ranges[i].end
                l = i + 1
        return -1

    def add_range(self, range):
        """! Add range
        @param self this object
        @param range range
        @return none
        """
        self.ranges.append(range)

    def get_all(self):
        """! Get all ranges
        @param self this object
        @return the ranges
        """
        return self.ranges

    def get_ranges(self, start, end):
        """! Get selected ranges
        @param self this object
        @param start range start
        @param end range end
        @return the range or and empty list
        """
        s = self.__search(start)
        e = self.__search(end)
        if s == -1 and e == -1:
            return []
        elif s == -1:
            return self.ranges[0 : e + 1]
        elif e == -1:
            return self.ranges[s : len(self.ranges)]
        else:
            return self.ranges[s : e + 1]

    def get_ranges_bounds(self, start, end):
        """! Get ranges bounds
        @param self this object
        @param start range start
        @param end range end
        @return range
        """
        s = self.__search(start)
        e = self.__search(end)
        if s == -1 and e == -1:
            return (0, 0)
        elif s == -1:
            return (0, e + 1)
        elif e == -1:
            return (s, len(self.ranges))
        else:
            return (s, e + 1)

    def sort(self):
        """! Sort ranges
        @param self this object
        @return none
        """
        self.ranges.sort(ranges_cmp)

    def get_bounds(self):
        """! Get bounds
        @param self this object
        @return the bounds
        """
        if len(self.ranges) > 0:
            lo = self.ranges[0].start
            hi = self.ranges[len(self.ranges) - 1].end
            return (lo, hi)
        else:
            return (0, 0)


## TimelineEvent class
class TimelineEvent:
    ## @var name
    #  name
    ## @var events
    #  events
    def __init__(self, name=""):
        """! Get ranges bounds
        @param self this object
        @param name name
        """
        self.name = name
        self.events = []

    def __search(self, key):
        """! Search function
        @param self this object
        @param key the key
        @return event index
        """
        l = 0
        u = len(self.events) - 1
        while l <= u:
            i = int((l + u) / 2)
            if key == self.events[i].at:
                return i
            elif key < self.events[i].at:
                u = i - 1
            else:
                # key > self.events[i].at
                l = i + 1
        return l

    def add_event(self, event):
        """! Add Event
        @param self this object
        @param event event to add
        @return none
        """
        self.events.append(event)

    def get_events(self, start, end):
        """! Get Events
        @param self this object
        @param start starting event
        @param end ending event
        @return the events
        """
        s = self.__search(start)
        e = self.__search(end)
        return self.events[s : e + 1]

    def get_events_bounds(self, start, end):
        """! Get Events Bounds
        @param self this object
        @param start starting event
        @param end ending event
        @return event bounds
        """
        s = self.__search(start)
        e = self.__search(end)
        return (s, e + 1)

    def sort(self):
        """! Sort function
        @param self this object
        @return none
        """
        self.events.sort(events_cmp)

    def get_bounds(self):
        """! Get Bounds
        @param self this object
        @return the bounds
        """
        if len(self.events) > 0:
            lo = self.events[0].at
            hi = self.events[-1].at
            return (lo, hi)
        else:
            return (0, 0)


## Timeline class
class Timeline:
    ## @var name
    #  name
    ## @var ranges
    #  ranges
    ## @var event_str
    #  event string
    ## @var event_int
    #  event int
    def __init__(self, name=""):
        """! Initializer
        @param self this object
        @param name name
        """
        self.ranges = []
        self.event_str = []
        self.event_int = []
        self.name = name

    def get_range(self, name):
        """! Get range
        @param self this object
        @param name name
        @return the range
        """
        for range in self.ranges:
            if range.name == name:
                return range
        timeline = TimelineDataRange(name)
        self.ranges.append(timeline)
        return timeline

    def get_event_str(self, name):
        """! Get Event String
        @param self this object
        @param name name
        @return the event string
        """
        for event_str in self.event_str:
            if event_str.name == name:
                return event_str
        timeline = TimelineEvent(name)
        self.event_str.append(timeline)
        return timeline

    def get_event_int(self, name):
        """! Get Event Int
        @param self this object
        @param name name
        @return eevent int
        """
        for event_int in self.event_int:
            if event_int.name == name:
                return event_int
        timeline = TimelineEvent(name)
        self.event_int.append(timeline)
        return timeline

    def get_ranges(self):
        """! Get Ranges
        @param self this object
        @return the ranges
        """
        return self.ranges

    def get_events_str(self):
        """! Get Events string
        @param self this object
        @return event string
        """
        return self.event_str

    def get_events_int(self):
        """! Get Events int
        @param self this object
        @return evrnt int
        """
        return self.event_int

    def sort(self):
        """! Sort the ranges and events
        @param self this object
        @return none
        """
        for range in self.ranges:
            range.sort()
        for event in self.event_int:
            event.sort()
        for event in self.event_str:
            event.sort()

    def get_bounds(self):
        """! Get Bounds
        @param self this object
        @return the bounds
        """
        lo = 0
        hi = 0
        for range in self.ranges:
            (range_lo, range_hi) = range.get_bounds()
            if range_lo < lo:
                lo = range_lo
            if range_hi > hi:
                hi = range_hi
        for event_str in self.event_str:
            (ev_lo, ev_hi) = event_str.get_bounds()
            if ev_lo < lo:
                lo = ev_lo
            if ev_hi > hi:
                hi = ev_hi
        for event_int in self.event_int:
            (ev_lo, ev_hi) = event_int.get_bounds()
            if ev_lo < lo:
                lo = ev_lo
            if ev_hi > hi:
                hi = ev_hi
        return (lo, hi)


## Timelines class
class Timelines:
    ## @var timelines
    #  timelines
    def __init__(self):
        """Initializer
        @param self: this object
        """
        self.timelines = []

    def get(self, name):
        """! Get Timeline
        @param self this object
        @param name name
        @return the timeline for the name
        """
        for timeline in self.timelines:
            if timeline.name == name:
                return timeline
        timeline = Timeline(name)
        self.timelines.append(timeline)
        return timeline

    def get_all(self):
        """! Get All Timeline
        @param self this object
        @return all timelines
        """
        return self.timelines

    def sort(self):
        """! Sort the timelines
        @param self this object
        @return none
        """
        for timeline in self.timelines:
            timeline.sort()

    def get_bounds(self):
        """! Get Bounds
        @param self this object
        @return the bounds for all timelines
        """
        lo = 0
        hi = 0
        for timeline in self.timelines:
            (t_lo, t_hi) = timeline.get_bounds()
            if t_lo < lo:
                lo = t_lo
            if t_hi > hi:
                hi = t_hi
        return (lo, hi)

    def get_all_range_values(self):
        """! Get All Ranges
        @param self this object
        @return the keys for all ranges
        """
        range_values = {}
        for timeline in self.timelines:
            for ranges in timeline.get_ranges():
                for ran in ranges.get_all():
                    range_values[ran.value] = 1
        return range_values.keys()


## Color class
class Color:
    ## @var r
    #  red
    ## @var g
    #  green
    ## @var b
    #  blue
    def __init__(self, r=0.0, g=0.0, b=0.0):
        """! Initializer
        @param self: this object
        @param r: red
        @param g: green
        @param b: blue
        """
        self.r = r
        self.g = g
        self.b = b

    def set(self, r, g, b):
        """! Set color
        @param self: this object
        @param r: red
        @param g: green
        @param b: blue
        @return none
        """
        self.r = r
        self.g = g
        self.b = b


## Colors class
class Colors:
    ## @var __colors
    #  colors
    ## @var default_colors
    #  default colors
    ## XXX add more
    default_colors = [
        Color(1, 0, 0),
        Color(0, 1, 0),
        Color(0, 0, 1),
        Color(1, 1, 0),
        Color(1, 0, 1),
        Color(0, 1, 1),
    ]

    def __init__(self):
        """! Initializer
        @param self this object
        """
        self.__colors = {}

    def add(self, name, color):
        """! Add
        @param self this object
        @param name name of the color
        @param color color value
        @return none
        """
        self.__colors[name] = color

    def lookup(self, name):
        """! Lookup name
        @param self this object
        @param name name
        @return named color
        """
        if not self.__colors.has_key(name):
            self.add(name, self.default_colors.pop())
        return self.__colors.get(name)


## TopLegendRenderer class
class TopLegendRenderer:
    ## @var __padding
    #  padding
    ## @var __legends
    #  legends
    ## @var __colors
    #  colors
    ## @var __width
    #  width
    ## @var __height
    #  height
    def __init__(self):
        """! Initializer
        @param self this object
        """
        self.__padding = 10

    def set_padding(self, padding):
        """! Set padding
        @param self this object
        @param padding padding
        @return none
        """
        self.__padding = padding

    def set_legends(self, legends, colors):
        """! Set padding
        @param self this object
        @param legends legends
        @param colors colors
        @return none
        """
        self.__legends = legends
        self.__colors = colors

    def layout(self, width):
        """! Set padding
        @param self this object
        @param width width
        @return none
        """
        self.__width = width
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)
        ctx = cairo.Context(surface)
        line_height = 0
        total_height = self.__padding
        line_used = self.__padding
        for legend in self.__legends:
            (t_width, t_height) = ctx.text_extents(legend)[2:4]
            item_width = self.__padding + self.__padding + t_width + self.__padding
            item_height = t_height + self.__padding
            if item_height > line_height:
                line_height = item_height
            if line_used + item_width > self.__width:
                line_used = self.__padding + item_width
                total_height += line_height
            else:
                line_used += item_width
            x = line_used - item_width
        total_height += line_height
        self.__height = total_height

    def get_height(self):
        """! Set padding
        @param self this object
        @return height
        """
        return self.__height

    def draw(self, ctx):
        """! Set padding
        @param self this object
        @param ctx ctx
        @return none
        """
        i = 0
        line_height = 0
        total_height = self.__padding
        line_used = self.__padding
        for legend in self.__legends:
            (t_width, t_height) = ctx.text_extents(legend)[2:4]
            item_width = self.__padding + self.__padding + t_width + self.__padding
            item_height = t_height + self.__padding
            if item_height > line_height:
                line_height = item_height
            if line_used + item_width > self.__width:
                line_used = self.__padding + item_width
                total_height += line_height
            else:
                line_used += item_width
            x = line_used - item_width
            ctx.rectangle(x, total_height, self.__padding, self.__padding)
            ctx.set_source_rgb(0, 0, 0)
            ctx.set_line_width(2)
            ctx.stroke_preserve()
            ctx.set_source_rgb(self.__colors[i].r, self.__colors[i].g, self.__colors[i].b)
            ctx.fill()
            ctx.move_to(x + self.__padding * 2, total_height + t_height)
            ctx.set_source_rgb(0, 0, 0)
            ctx.show_text(legend)
            i += 1


## TimelinesRenderer class
class TimelinesRenderer:
    ## @var padding
    #  padding
    ## @var timelines
    #  timelines
    ## @var colors
    #  colors
    ## @var start
    #  start
    ## @var end
    #  end
    ## @var left_width
    #  left width
    ## @var right_width
    #  right width
    ## @var max_text_height
    #  maximum text height
    ## @var width
    #  width
    ## @var height
    #  height
    ## @var grey_background
    #  grey background
    def __init__(self):
        """! Initializer
        @param self this object
        """
        self.padding = 10
        return

    def get_height(self):
        """! Get Height
        @param self this object
        @return height
        """
        return self.height

    def set_timelines(self, timelines, colors):
        """! Set Timelines
        @param self this object
        @param timelines timelines
        @param colors colors
        @return none
        """
        self.timelines = timelines
        self.colors = colors

    def set_render_range(self, start, end):
        """! Set Render Range
        @param self this object
        @param start start
        @param end end
        @return none
        """
        self.start = start
        self.end = end

    def get_data_x_start(self):
        """! Get Data X Start
        @param self: this object
        @return X start
        """
        return (
            self.padding / 2 + self.left_width + self.padding + self.right_width + self.padding / 2
        )

    def layout(self, width):
        """! Get Data X Start
        @param self this object
        @param width width
        @return none
        """
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)
        ctx = cairo.Context(surface)
        max_text_height = ctx.text_extents(
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcedefghijklmnopqrstuvwxyz0123456789"
        )[3]

        left_width = 0
        right_width = 0
        left_n_lines = 0
        range_n = 0
        eventint_n = 0
        eventstr_n = 0
        for timeline in self.timelines.get_all():
            left_n_lines += 1
            t_width = ctx.text_extents(timeline.name)[2]
            left_width = max(left_width, t_width)
            for rang in timeline.get_ranges():
                t_width = ctx.text_extents(rang.name)[2]
                right_width = max(right_width, t_width)
                range_n += 1
            for events_int in timeline.get_events_int():
                t_width = ctx.text_extents(events_int.name)[2]
                right_width = max(right_width, t_width)
                eventint_n += 1
            for events_str in timeline.get_events_str():
                t_width = ctx.text_extents(events_str.name)[2]
                right_width = max(right_width, t_width)
                eventstr_n += 1

        left_height = left_n_lines * max_text_height + (left_n_lines - 1) * self.padding
        right_n_lines = range_n + eventint_n + eventstr_n
        right_height = (right_n_lines - 1) * self.padding + right_n_lines * max_text_height
        right_data_height = (eventint_n + eventstr_n) * (max_text_height + 5) + range_n * 10
        right_data_height += (right_n_lines - 1) * self.padding

        height = max(left_height, right_height)
        height = max(height, right_data_height)

        self.left_width = left_width
        self.right_width = right_width
        self.max_text_height = max_text_height
        self.width = width
        self.height = height + self.padding

    def draw_line(self, ctx, x, y, width, height):
        """! Draw Line
        @param self this object
        @param ctx ctx
        @param x x
        @param y y
        @param width width
        @param height height
        @return none
        """
        ctx.move_to(x, y)
        ctx.rel_line_to(width, height)
        ctx.close_path()
        ctx.set_operator(cairo.OPERATOR_SOURCE)
        ctx.set_line_width(1.0)
        ctx.set_source_rgb(0, 0, 0)
        ctx.stroke()

    def draw_events(self, ctx, events, x, y, width, height):
        """! Draw Event
        @param self this object
        @param ctx ctx
        @param events events
        @param x x
        @param y y
        @param width width
        @param height height
        @return none
        """
        if (self.grey_background % 2) == 0:
            ctx.rectangle(x, y - self.padding / 2, width, height + self.padding)
            ctx.set_source_rgb(0.9, 0.9, 0.9)
            ctx.fill()
        last_x_drawn = int(x)
        (lo, hi) = events.get_events_bounds(self.start, self.end)
        for event in events.events[lo:hi]:
            real_x = int(x + (event.at - self.start) * width / (self.end - self.start))
            if real_x > last_x_drawn + 2:
                ctx.rectangle(real_x, y, 1, 1)
                ctx.set_source_rgb(1, 0, 0)
                ctx.stroke()
                ctx.move_to(real_x, y + self.max_text_height)
                ctx.set_source_rgb(0, 0, 0)
                ctx.show_text(str(event.value))
                last_x_drawn = real_x
        self.grey_background += 1

    def draw_ranges(self, ctx, ranges, x, y, width, height):
        """! Draw Ranges
        @param self this object
        @param ctx ctx
        @param ranges ranges
        @param x x
        @param y y
        @param width width
        @param height height
        @return none
        """
        if (self.grey_background % 2) == 0:
            ctx.rectangle(x, y - self.padding / 2, width, height + self.padding)
            ctx.set_source_rgb(0.9, 0.9, 0.9)
            ctx.fill()
        last_x_drawn = int(x - 1)
        (lo, hi) = ranges.get_ranges_bounds(self.start, self.end)
        for data_range in ranges.ranges[lo:hi]:
            s = max(data_range.start, self.start)
            e = min(data_range.end, self.end)
            x_start = int(x + (s - self.start) * width / (self.end - self.start))
            x_end = int(x + (e - self.start) * width / (self.end - self.start))
            if x_end > last_x_drawn:
                ctx.rectangle(x_start, y, x_end - x_start, 10)
                ctx.set_source_rgb(0, 0, 0)
                ctx.stroke_preserve()
                color = self.colors.lookup(data_range.value)
                ctx.set_source_rgb(color.r, color.g, color.b)
                ctx.fill()
                last_x_drawn = x_end

        self.grey_background += 1

    def draw(self, ctx):
        """! Draw
        @param self this object
        @param ctx ctx
        @return none
        """
        timeline_top = 0
        top_y = self.padding / 2
        left_x_start = self.padding / 2
        left_x_end = left_x_start + self.left_width
        right_x_start = left_x_end + self.padding
        right_x_end = right_x_start + self.right_width
        data_x_start = right_x_end + self.padding / 2
        data_x_end = self.width
        data_width = data_x_end - data_x_start
        cur_y = top_y
        self.draw_line(ctx, 0, 0, self.width, 0)
        self.grey_background = 1
        for timeline in self.timelines.get_all():
            (y_bearing, t_width, t_height) = ctx.text_extents(timeline.name)[1:4]
            ctx.move_to(left_x_start, cur_y + self.max_text_height - (t_height + y_bearing))
            ctx.show_text(timeline.name)
            for events_int in timeline.get_events_int():
                (y_bearing, t_width, t_height) = ctx.text_extents(events_int.name)[1:4]
                ctx.move_to(right_x_start, cur_y + self.max_text_height - (t_height + y_bearing))
                ctx.show_text(events_int.name)
                self.draw_events(
                    ctx, events_int, data_x_start, cur_y, data_width, self.max_text_height + 5
                )
                cur_y += self.max_text_height + 5 + self.padding
                self.draw_line(
                    ctx,
                    right_x_start - self.padding / 2,
                    cur_y - self.padding / 2,
                    self.right_width + self.padding,
                    0,
                )

            for events_str in timeline.get_events_str():
                (y_bearing, t_width, t_height) = ctx.text_extents(events_str.name)[1:4]
                ctx.move_to(right_x_start, cur_y + self.max_text_height - (t_height + y_bearing))
                ctx.show_text(events_str.name)
                self.draw_events(
                    ctx, events_str, data_x_start, cur_y, data_width, self.max_text_height + 5
                )
                cur_y += self.max_text_height + 5 + self.padding
                self.draw_line(
                    ctx,
                    right_x_start - self.padding / 2,
                    cur_y - self.padding / 2,
                    self.right_width + self.padding,
                    0,
                )
            for ranges in timeline.get_ranges():
                (y_bearing, t_width, t_height) = ctx.text_extents(ranges.name)[1:4]
                ctx.move_to(right_x_start, cur_y + self.max_text_height - (t_height + y_bearing))
                ctx.show_text(ranges.name)
                self.draw_ranges(ctx, ranges, data_x_start, cur_y, data_width, 10)
                cur_y += self.max_text_height + self.padding
                self.draw_line(
                    ctx,
                    right_x_start - self.padding / 2,
                    cur_y - self.padding / 2,
                    self.right_width + self.padding,
                    0,
                )
            self.draw_line(ctx, 0, cur_y - self.padding / 2, self.width, 0)
        bot_y = cur_y - self.padding / 2
        self.draw_line(ctx, left_x_end + self.padding / 2, 0, 0, bot_y)
        self.draw_line(ctx, right_x_end + self.padding / 2, 0, 0, bot_y)


## ScaleRenderer class
class ScaleRenderer:
    ## @var __top
    #  top
    ## @var __lo
    #  lo
    ## @var __hi
    #  hi
    ## @var __delta
    #  delta
    ## @var __width
    #  width
    ## @var __height
    #  height
    ## @var max_text_height
    #  maximum text height
    def __init__(self):
        """! Initializer
        @param self this object
        """
        self.__top = 0
        return

    def set_bounds(self, lo, hi):
        """! Set Bounds
        @param self this object
        @param lo lo
        @param hi hi
        @return none
        """
        self.__lo = lo
        self.__hi = hi

    def get_position(self, x):
        """! Get Position
        @param self this object
        @param x x
        @return real x
        """
        real_x = (x - self.__lo) * self.__width / (self.__hi - self.__lo)
        return real_x

    def set_top(self):
        """! Set Top
        @param self this object
        @return none
        """
        self.__top = 1

    def set_bot(self):
        """! Set Bottom
        @param self this object
        @return none
        """
        self.__top = 0

    def layout(self, width):
        """! Layout
        @param self this object
        @param width width
        @return none
        """
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)
        ctx = cairo.Context(surface)

        # calculate scale delta
        data_delta = self.__hi - self.__lo
        closest = 1
        while (closest * 10) < data_delta:
            closest *= 10
        if (data_delta / closest) == 0:
            delta = closest
        elif (data_delta / closest) == 1:
            delta = closest / 10
        else:
            delta = closest
        start = self.__lo - (self.__lo % delta) + delta
        end = self.__hi - (self.__hi % delta)

        self.__delta = delta
        self.__width = width

        # calculate text height
        max_text_height = ctx.text_extents(
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcedefghijklmnopqrstuvwxyz0123456789"
        )[3]
        self.max_text_height = max_text_height
        height = max_text_height + 10
        self.__height = height

    def get_height(self):
        """! Get Height
        @param self: this object
        @return height
        """
        return self.__height

    def draw(self, ctx):
        """! Draw
        @param self this object
        @param ctx ctx
        @return none
        """
        delta = self.__delta
        start = self.__lo - (self.__lo % delta) + delta
        end = self.__hi - (self.__hi % delta)

        if self.__top == 1:
            s = -1
        else:
            s = 1
        # print scale points
        ctx.set_source_rgb(0, 0, 0)
        ctx.set_line_width(1.0)
        ticks = range(int(start), int(end + delta), int(delta))
        for x in ticks:
            real_x = (x - self.__lo) * self.__width / (self.__hi - self.__lo)
            ctx.move_to(real_x, 0)
            ctx.line_to(real_x, 5 * s)
            ctx.close_path()
            ctx.stroke()
            (t_y_bearing, t_width, t_height) = ctx.text_extents(str(x))[1:4]
            if self.__top:
                text_delta = t_height + t_y_bearing
            else:
                text_delta = -t_y_bearing
            ctx.move_to(real_x - t_width / 2, (5 + 5 + text_delta) * s)
            ctx.show_text(str(x))
        # draw subticks
        delta /= 10
        if delta > 0:
            start = self.__lo - (self.__lo % delta) + delta
            end = self.__hi - (self.__hi % delta)
            for x in range(int(start), int(end + delta), int(delta)):
                real_x = (x - self.__lo) * self.__width / (self.__hi - self.__lo)
                ctx.move_to(real_x, 0)
                ctx.line_to(real_x, 3 * s)
                ctx.close_path()
                ctx.stroke()


## GraphicRenderer class
class GraphicRenderer:
    ## @var __start
    #  start
    ## @var __end
    #  end
    ## @var __mid_scale
    #  mid scale
    ## @var __bot_scale
    #  bottom scale
    ## @var __width
    #  width
    ## @var __height
    #  height
    ## @var __r_start
    #  start
    ## @var __r_end
    #  end
    ## @var __data
    #  data
    ## @var __mid_scale
    #  mid scale
    ## @var __top_legend
    #  top legend
    def __init__(self, start, end):
        """! Initializer
        @param self this object
        @param start start
        @param end end
        """
        self.__start = float(start)
        self.__end = float(end)
        self.__mid_scale = ScaleRenderer()
        self.__mid_scale.set_top()
        self.__bot_scale = ScaleRenderer()
        self.__bot_scale.set_bounds(start, end)
        self.__bot_scale.set_bot()
        self.__width = 1
        self.__height = 1

    def get_width(self):
        """! Get Width
        @param self: this object
        @return width
        """
        return self.__width

    def get_height(self):
        """! Get Height
        @param self this object
        @return height
        """
        return self.__height

    # return x, y, width, height
    def get_data_rectangle(self):
        """! Get Data Rectangle
        @param self this object
        @return rectangle
        """
        y_start = self.__top_legend.get_height()
        x_start = self.__data.get_data_x_start()
        return (x_start, y_start, self.__width - x_start, self.__data.get_height())

    def scale_data(self, x):
        """! Get Data Rectangle
        @param self this object
        @param x x
        @return scaled x
        """
        x_start = self.__data.get_data_x_start()
        x_scaled = x / (self.__width - x_start) * (self.__r_end - self.__r_start)
        return x_scaled

    # return x, y, width, height
    def get_selection_rectangle(self):
        """! Get Selection Rectangle
        @param self this object
        @return rectangle
        """
        y_start = (
            self.__top_legend.get_height()
            + self.__data.get_height()
            + self.__mid_scale.get_height()
            + 20
        )
        y_height = self.__bot_scale.get_height() + 20
        x_start = self.__bot_scale.get_position(self.__r_start)
        x_end = self.__bot_scale.get_position(self.__r_end)
        return (x_start, y_start, x_end - x_start, y_height)

    def scale_selection(self, x):
        """! Scale Selection
        @param self this object
        @param x the X
        @return scaled X
        """
        x_scaled = x / self.__width * (self.__end - self.__start)
        return x_scaled

    def set_range(self, start, end):
        """! Set Range
        @param self this object
        @param start start
        @param end end
        @return none
        """
        s = min(start, end)
        e = max(start, end)
        start = max(self.__start, s)
        end = min(self.__end, e)
        self.__r_start = start
        self.__r_end = end
        self.__data.set_render_range(start, end)
        self.__mid_scale.set_bounds(start, end)
        self.layout(self.__width, self.__height)

    def get_range(self):
        """! Get Range
        @param self this object
        @return range
        """
        return (self.__r_start, self.__r_end)

    def set_data(self, data):
        """! Set Date
        @param self this object
        @param data data
        @return none
        """
        self.__data = data

    def set_top_legend(self, top_legend):
        """! Set Top Legend
        @param self this object
        @param top_legend The legend
        @return none
        """
        self.__top_legend = top_legend

    def layout(self, width, height):
        """! Set Layout
        @param self this object
        @param width width
        @param height height
        @return none
        """
        self.__width = width
        self.__height = height
        self.__top_legend.layout(width)
        top_legend_height = self.__top_legend.get_height()
        self.__data.layout(width)
        self.__mid_scale.layout(width - self.__data.get_data_x_start())
        self.__bot_scale.layout(width)
        return

    def __x_pixel(self, x, width):
        """! X Pixel
        @param self this object
        @param x x
        @param width width
        @return x pixel
        """
        new_x = (x - self.__start) * width / (self.__end - self.__start)
        return new_x

    def draw(self, ctx):
        """! Draw
        @param self this object
        @param ctx ctx
        @return none
        """
        # default background is white
        ctx.save()
        ctx.set_source_rgb(1, 1, 1)
        ctx.set_operator(cairo.OPERATOR_SOURCE)
        ctx.rectangle(0, 0, self.__width, self.__height)
        ctx.fill()

        # top legend
        ctx.save()
        self.__top_legend.draw(ctx)
        top_legend_height = self.__top_legend.get_height()
        ctx.restore()

        # separation line
        ctx.move_to(0, top_legend_height)
        ctx.line_to(self.__width, top_legend_height)
        ctx.close_path()
        ctx.set_line_width(2)
        ctx.set_source_rgb(0, 0, 0)
        ctx.stroke()

        # data
        ctx.save()
        ctx.translate(0, top_legend_height)
        self.__data.draw(ctx)
        ctx.restore()

        # scale below data
        ctx.save()
        ctx.translate(
            self.__data.get_data_x_start(),
            top_legend_height + self.__data.get_height() + self.__mid_scale.get_height(),
        )
        self.__mid_scale.draw(ctx)
        ctx.restore()

        height_used = top_legend_height + self.__data.get_height() + self.__mid_scale.get_height()

        # separation between scale and left pane
        ctx.move_to(self.__data.get_data_x_start(), height_used)
        ctx.rel_line_to(0, -self.__mid_scale.get_height())
        ctx.close_path()
        ctx.set_source_rgb(0, 0, 0)
        ctx.set_line_width(2)
        ctx.stroke()

        # separation below scale
        ctx.move_to(0, height_used)
        ctx.line_to(self.__width, height_used)
        ctx.close_path()
        ctx.set_line_width(2)
        ctx.set_source_rgb(0, 0, 0)
        ctx.stroke()

        select_start = self.__bot_scale.get_position(self.__r_start)
        select_end = self.__bot_scale.get_position(self.__r_end)

        # left connection between top scale and bottom scale
        ctx.move_to(0, height_used)
        ctx.line_to(self.__data.get_data_x_start(), height_used)
        ctx.line_to(select_start, height_used + 20)
        ctx.line_to(0, height_used + 20)
        ctx.line_to(0, height_used)
        ctx.set_source_rgb(0, 0, 0)
        ctx.set_line_width(1)
        ctx.stroke_preserve()
        ctx.set_source_rgb(0.9, 0.9, 0.9)
        ctx.fill()

        # right connection between top scale and bottom scale
        ctx.move_to(self.__width, height_used)
        ctx.line_to(self.__width, height_used + 20)
        ctx.line_to(select_end, height_used + 20)
        ctx.line_to(self.__width, height_used)
        ctx.set_source_rgb(0, 0, 0)
        ctx.set_line_width(1)
        ctx.stroke_preserve()
        ctx.set_source_rgb(0.9, 0.9, 0.9)
        ctx.fill()

        height_used += 20

        # unused area background
        unused_start = self.__bot_scale.get_position(self.__r_start)
        unused_end = self.__bot_scale.get_position(self.__r_end)
        unused_height = self.__bot_scale.get_height() + 20
        ctx.rectangle(0, height_used, unused_start, unused_height)
        ctx.rectangle(unused_end, height_used, self.__width - unused_end, unused_height)
        ctx.set_source_rgb(0.9, 0.9, 0.9)
        ctx.fill()

        # border line around bottom scale
        ctx.move_to(unused_end, height_used)
        ctx.line_to(self.__width, height_used)
        ctx.line_to(self.__width, height_used + unused_height)
        ctx.line_to(0, height_used + unused_height)
        ctx.line_to(0, height_used)
        ctx.line_to(unused_start, height_used)
        ctx.close_path()
        ctx.set_line_width(2)
        ctx.set_source_rgb(0, 0, 0)
        ctx.stroke()
        ctx.move_to(unused_start, height_used)
        ctx.line_to(unused_end, height_used)
        ctx.close_path()
        ctx.set_line_width(1)
        ctx.set_source_rgb(0.9, 0.9, 0.9)
        ctx.stroke()

        # unused area dot borders
        ctx.save()
        ctx.move_to(max(unused_start, 2), height_used)
        ctx.rel_line_to(0, unused_height)
        ctx.move_to(min(unused_end, self.__width - 2), height_used)
        ctx.rel_line_to(0, unused_height)
        ctx.set_dash([5], 0)
        ctx.set_source_rgb(0, 0, 0)
        ctx.set_line_width(1)
        ctx.stroke()
        ctx.restore()

        # bottom scale
        ctx.save()
        ctx.translate(0, height_used)
        self.__bot_scale.draw(ctx)
        ctx.restore()


## GtkGraphicRenderer class
class GtkGraphicRenderer(gtk.DrawingArea):
    ## @var __data
    #  data
    ## @var __moving_left
    #  moving left
    ## @var __moving_right
    #  moving right
    ## @var __moving_both
    #  moving both
    ## @var __moving_top
    #  moving top
    ## @var __force_full_redraw
    #  full redraw
    ## @var __moving_left_cur
    #  moving left cur
    ## @var __moving_right_cur
    #  moving right cur
    ## @var __moving_both_start
    #  moving both start
    ## @var __moving_both_cur
    #  moving both cur
    ## @var __moving_top_cur
    #  moving top cur
    ## @var __moving_top_start
    #  moving top start
    ## @var __width
    #  width
    ## @var __height
    #  height
    ## @var __buffer_surface
    # __buffer_surface
    ## @var expose
    #  expose function
    ## @var size_allocate
    #  size_allocate function
    ## @var motion_notify
    #  motion_notify function
    ## @var button_press
    #  button_press function
    ## @var button_release
    #  button_release function
    def __init__(self, data):
        """! Initializer
        @param self this object
        @param data data
        """
        super(GtkGraphicRenderer, self).__init__()
        self.__data = data
        self.__moving_left = False
        self.__moving_right = False
        self.__moving_both = False
        self.__moving_top = False
        self.__force_full_redraw = True
        self.add_events(gtk.gdk.POINTER_MOTION_MASK)
        self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
        self.add_events(gtk.gdk.BUTTON_RELEASE_MASK)
        self.connect("expose_event", self.expose)
        self.connect("size-allocate", self.size_allocate)
        self.connect("motion-notify-event", self.motion_notify)
        self.connect("button-press-event", self.button_press)
        self.connect("button-release-event", self.button_release)

    def set_smaller_zoom(self):
        """! Set Smaller Zoom
        @param self this object
        @return none
        """
        (start, end) = self.__data.get_range()
        self.__data.set_range(start, start + (end - start) * 2)
        self.__force_full_redraw = True
        self.queue_draw()

    def set_bigger_zoom(self):
        """! Set Bigger Zoom
        @param self this object
        @return none
        """
        (start, end) = self.__data.get_range()
        self.__data.set_range(start, start + (end - start) / 2)
        self.__force_full_redraw = True
        self.queue_draw()

    def output_png(self, filename):
        """! Output PNG
        @param self this object
        @param filename file name
        @return none
        """
        surface = cairo.ImageSurface(
            cairo.FORMAT_ARGB32, self.__data.get_width(), self.__data.get_height()
        )
        ctx = cairo.Context(self.__buffer_surface)
        self.__data.draw(ctx)
        surface.write_to_png(filename)

    def button_press(self, widget, event):
        """! Button Press
        @param self this object
        @param widget widget
        @param event event
        @return true if button has been pressed otherwise false
        """
        (x, y, width, height) = self.__data.get_selection_rectangle()
        (d_x, d_y, d_width, d_height) = self.__data.get_data_rectangle()
        if event.y > y and event.y < y + height:
            if abs(event.x - x) < 5:
                self.__moving_left = True
                return True
            if abs(event.x - (x + width)) < 5:
                self.__moving_right = True
                return True
            if event.x > x and event.x < x + width:
                self.__moving_both = True
                self.__moving_both_start = event.x
                self.__moving_both_cur = event.x
                return True
        if event.y > d_y and event.y < (d_y + d_height):
            if event.x > d_x and event.x < (d_x + d_width):
                self.__moving_top = True
                self.__moving_top_start = event.x
                self.__moving_top_cur = event.x
                return True
        return False

    def button_release(self, widget, event):
        """! Button Release
        @param self this object
        @param widget widget
        @param event event
        @return true if button was released otherwise false
        """
        if self.__moving_left:
            self.__moving_left = False
            left = self.__data.scale_selection(self.__moving_left_cur)
            right = self.__data.get_range()[1]
            self.__data.set_range(left, right)
            self.__force_full_redraw = True
            self.queue_draw()
            return True
        if self.__moving_right:
            self.__moving_right = False
            right = self.__data.scale_selection(self.__moving_right_cur)
            left = self.__data.get_range()[0]
            self.__data.set_range(left, right)
            self.__force_full_redraw = True
            self.queue_draw()
            return True
        if self.__moving_both:
            self.__moving_both = False
            delta = self.__data.scale_selection(self.__moving_both_cur - self.__moving_both_start)
            (left, right) = self.__data.get_range()
            self.__data.set_range(left + delta, right + delta)
            self.__force_full_redraw = True
            self.queue_draw()
            return True
        if self.__moving_top:
            self.__moving_top = False
        return False

    def motion_notify(self, widget, event):
        """! Motion Notify
        @param self this object
        @param widget widget
        @param event event
        @return true if moving otherwise false
        """
        (x, y, width, height) = self.__data.get_selection_rectangle()
        if self.__moving_left:
            if event.x <= 0:
                self.__moving_left_cur = 0
            elif event.x >= x + width:
                self.__moving_left_cur = x + width
            else:
                self.__moving_left_cur = event.x
            self.queue_draw_area(0, int(y), int(self.__width), int(height))
            return True
        if self.__moving_right:
            if event.x >= self.__width:
                self.__moving_right = self.__width
            elif event.x < x:
                self.__moving_right_cur = x
            else:
                self.__moving_right_cur = event.x
            self.queue_draw_area(0, int(y), int(self.__width), int(height))
            return True
        if self.__moving_both:
            cur_e = self.__width - (x + width - self.__moving_both_start)
            cur_s = self.__moving_both_start - x
            if event.x < cur_s:
                self.__moving_both_cur = cur_s
            elif event.x > cur_e:
                self.__moving_both_cur = cur_e
            else:
                self.__moving_both_cur = event.x
            self.queue_draw_area(0, int(y), int(self.__width), int(height))
            return True
        if self.__moving_top:
            self.__moving_top_cur = event.x
            delta = self.__data.scale_data(self.__moving_top_start - self.__moving_top_cur)
            (left, right) = self.__data.get_range()
            self.__data.set_range(left + delta, right + delta)
            self.__force_full_redraw = True
            self.__moving_top_start = event.x
            self.queue_draw()
            return True
        (d_x, d_y, d_width, d_height) = self.__data.get_data_rectangle()
        if event.y > y and event.y < y + height:
            if abs(event.x - x) < 5 or abs(event.x - (x + width)) < 5:
                widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.SB_H_DOUBLE_ARROW))
                return True
            if event.x > x and event.x < x + width:
                widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
                return True
        if event.y > d_y and event.y < (d_y + d_height):
            if event.x > d_x and event.x < (d_x + d_width):
                widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
                return True
        widget.window.set_cursor(None)
        return False

    def size_allocate(self, widget, allocation):
        """! Size Allocate
        @param self this object
        @param widget widget
        @param allocation allocation
        @return none
        """
        self.__width = allocation.width
        self.__height = allocation.height
        self.__data.layout(allocation.width, allocation.height)
        self.__force_full_redraw = True
        self.queue_draw()

    def expose(self, widget, event):
        """! Expose
        @param self this object
        @param widget widget
        @param event event
        @return false
        """
        if self.__force_full_redraw:
            self.__buffer_surface = cairo.ImageSurface(
                cairo.FORMAT_ARGB32, self.__data.get_width(), self.__data.get_height()
            )
            ctx = cairo.Context(self.__buffer_surface)
            self.__data.draw(ctx)
            self.__force_full_redraw = False
        ctx = widget.window.cairo_create()
        ctx.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
        ctx.clip()
        ctx.set_source_surface(self.__buffer_surface)
        ctx.paint()
        (x, y, width, height) = self.__data.get_selection_rectangle()
        if self.__moving_left:
            ctx.move_to(max(self.__moving_left_cur, 2), y)
            ctx.rel_line_to(0, height)
            ctx.close_path()
            ctx.set_line_width(1)
            ctx.set_source_rgb(0, 0, 0)
            ctx.stroke()
        if self.__moving_right:
            ctx.move_to(min(self.__moving_right_cur, self.__width - 2), y)
            ctx.rel_line_to(0, height)
            ctx.close_path()
            ctx.set_line_width(1)
            ctx.set_source_rgb(0, 0, 0)
            ctx.stroke()
        if self.__moving_both:
            delta_x = self.__moving_both_cur - self.__moving_both_start
            left_x = x + delta_x
            ctx.move_to(x + delta_x, y)
            ctx.rel_line_to(0, height)
            ctx.close_path()
            ctx.move_to(x + width + delta_x, y)
            ctx.rel_line_to(0, height)
            ctx.close_path()
            ctx.set_source_rgb(0, 0, 0)
            ctx.set_line_width(1)
            ctx.stroke()
        return False


## MainWindow class
class MainWindow:
    ## @var __window
    #  window
    ## @var __render
    #  render
    ## @var __dialog
    #  dialog
    ## @var __set_smaller_cb
    #  __set_smaller_cb function
    ## @var __set_bigger_cb
    #  __set_bigger_cb function
    ## @var __output_png_cb
    #  __output_png_cb function
    ## @var __dialog_response_cb
    #  __dialog_response_cb function
    def __init__(self):
        """! Initializer
        @param self this object
        """
        return

    def run(self, graphic):
        """! Run function
        @param self this object
        @param graphic graphic
        @return none
        """
        window = gtk.Window()
        self.__window = window
        window.set_default_size(200, 200)
        vbox = gtk.VBox()
        window.add(vbox)
        render = GtkGraphicRenderer(graphic)
        self.__render = render
        vbox.pack_end(render, True, True, 0)
        hbox = gtk.HBox()
        vbox.pack_start(hbox, False, False, 0)
        smaller_zoom = gtk.Button("Zoom Out")
        smaller_zoom.connect("clicked", self.__set_smaller_cb)
        hbox.pack_start(smaller_zoom)
        bigger_zoom = gtk.Button("Zoom In")
        bigger_zoom.connect("clicked", self.__set_bigger_cb)
        hbox.pack_start(bigger_zoom)
        output_png = gtk.Button("Output Png")
        output_png.connect("clicked", self.__output_png_cb)
        hbox.pack_start(output_png)
        window.connect("destroy", gtk.main_quit)
        window.show_all()
        # gtk.bindings_activate(gtk.main_quit, 'q', 0)
        gtk.main()

    def __set_smaller_cb(self, widget):
        """! Set Smaller Callback
        @param self this object
        @param widget widget
        @return none
        """
        self.__render.set_smaller_zoom()

    def __set_bigger_cb(self, widget):
        """! Set Bigger Callback
        @param self this object
        @param widget widget
        @return none
        """
        self.__render.set_bigger_zoom()

    def __output_png_cb(self, widget):
        """! Output PNG Callback
        @param self this object
        @param widget widget
        @return none
        """
        dialog = gtk.FileChooserDialog(
            "Output Png", self.__window, gtk.FILE_CHOOSER_ACTION_SAVE, ("Save", 1)
        )
        self.__dialog = dialog
        dialog.set_default_response(1)
        dialog.connect("response", self.__dialog_response_cb)
        dialog.show()

    def __dialog_response_cb(self, widget, response):
        """! Dialog Response Callback
        @param self this object
        @param widget widget
        @param response response
        @return none
        """
        if response == 1:
            filename = self.__dialog.get_filename()
            self.__render.output_png(filename)
            widget.hide()


## read_data function
def read_data(filename):
    timelines = Timelines()
    colors = Colors()
    m1 = re.compile("range ([^ ]+) ([^ ]+) ([^ ]+) ([0-9]+) ([0-9]+)")
    m2 = re.compile("event-str ([^ ]+) ([^ ]+) ([^ ]+) ([0-9]+)")
    m3 = re.compile("event-int ([^ ]+) ([^ ]+) ([0-9]+) ([0-9]+)")
    m4 = re.compile("color ([^ ]+) #([a-fA-F0-9]{2,2})([a-fA-F0-9]{2,2})([a-fA-F0-9]{2,2})")

    with open(filename, encoding="utf-8") as fh:
        for line in fh.readlines():
            m = m1.match(line)
            if m:
                line_name = m.group(1)
                timeline = timelines.get(m.group(1))
                rang = timeline.get_range(m.group(2))
                data_range = DataRange()
                data_range.value = m.group(3)
                data_range.start = int(m.group(4))
                data_range.end = int(m.group(5))
                rang.add_range(data_range)
                continue
            m = m2.match(line)
            if m:
                line_name = m.group(1)
                timeline = timelines.get(m.group(1))
                ev = timeline.get_event_str(m.group(2))
                event = EventString()
                event.value = m.group(3)
                event.at = int(m.group(4))
                ev.add_event(event)
                continue
            m = m3.match(line)
            if m:
                line_name = m.group(1)
                timeline = timelines.get(m.group(1))
                ev = timeline.get_event_int(m.group(2))
                event = EventInt()
                event.value = int(m.group(3))
                event.at = int(m.group(4))
                ev.add_event(event)
                continue

            m = m4.match(line)
            if m:
                r = int(m.group(2), 16)
                g = int(m.group(3), 16)
                b = int(m.group(4), 16)
                color = Color(r / 255, g / 255, b / 255)
                colors.add(m.group(1), color)
                continue
    timelines.sort()
    return (colors, timelines)


def main():
    (colors, timelines) = read_data(sys.argv[1])
    (lower_bound, upper_bound) = timelines.get_bounds()
    graphic = GraphicRenderer(lower_bound, upper_bound)
    top_legend = TopLegendRenderer()
    range_values = timelines.get_all_range_values()
    range_colors = []
    for range_value in range_values:
        range_colors.append(colors.lookup(range_value))
    top_legend.set_legends(range_values, range_colors)
    graphic.set_top_legend(top_legend)
    data = TimelinesRenderer()
    data.set_timelines(timelines, colors)
    graphic.set_data(data)

    # default range
    range_mid = (upper_bound - lower_bound) / 2
    range_width = (upper_bound - lower_bound) / 10
    range_lo = range_mid - range_width / 2
    range_hi = range_mid + range_width / 2
    graphic.set_range(range_lo, range_hi)

    main_window = MainWindow()
    main_window.run(graphic)


main()
