#!/usr/bin/python
import rayon
from rayon.rytools import *
from rayon.tickspec_parse import parse_tickspec
from rayon import miscutils, toolbox
import datetime

NO_VALUE = Constant('NO_VALUE')
def supplied(val):
    return val != NO_VALUE

def debug(msg):
    if rayon.DEBUG:
        tstamp = datetime.datetime.now()
        print "%s: %s" % (tstamp.strftime("%H:%M:%S"), msg)

def dbg_pprint(msg, d):
    if rayon.DEBUG:
        debug(msg)
        debug("-" * 25)
        pprint(d)
        debug("-" * 25)

def dbg_dataset(msg, d):
    if rayon.DEBUG:
        debug(msg)
        debug("-" * 25)
        print d.to_string()
        debug("-" * 25)

def get_log_scale(tools, opts):
    if opts['val_scale_min'] is None:
        return tools.new_scale('log', a_lo=1)
    else:
        return tools.new_scale('log', opts['val_scale_min'])

def get_scale(tools, opts):
    if opts['val_scale_min'] is None:
        scale_min = 0
    else:
        scale_min = opts['val_scale_min']
    return tools.new_scale(opts['val_scale'], a_lo=scale_min)

def get_category_tick_border(tools, opts, cat_scale, border_type,
                             line_type, tickmarker):
    if opts['no_cat_ticks']:
        return None
    # TODO: --cat-border
    return tools.new_border(border_type, scale=cat_scale,
                            line_type=line_type, tickmarker=tickmarker)

def get_value_tick_border(tools, opts, col, val_scale, border_type,
                          line_type, tickmarker):
    tick_positions = tuple(reduce(lambda a,b: a+b,
                          (f([col], val_scale) for f in opts['val_ticks'])))
    tick_tuples = tuple((val, None) for val in tick_positions)
    return tools.new_border(border_type, scale=val_scale,
                            line_type=line_type,
                            tickmarker=tickmarker,
                            tick_tuples=tick_tuples)

    
def OLDget_value_tick_border(tools, opts, col, val_scale, border_type,
                          line_type, tickmarker):
    tick_positions = val_scale.get_nice_tick_positions(opts['val_num_ticks'])
    tick_tuples = tuple((val, None) for val in tick_positions)
    #for fn in opts['val_ticks']:
    #    # Note that val_ticks is an array of values representing
    #    # _ticksets_ (i.e., lists), not individual tick
    #    # marks. Therefore, we concatenate them into one big list.
    #    tick_tuples.extend([(val, None) for val in fn(col)])
    return tools.new_border(border_type, scale=val_scale,
                            line_type=line_type,
                            tickmarker=tickmarker,
                            tick_tuples=tick_tuples)


# TODO: this should't be "cat_*", it should be "x_*"
def get_x_tickmarker(tools, opts):
    return tools.new_tickmarker('horizontal',
                                marker_args=dict(below=opts['cat_tick_size'],
                                                 above=0),
                                label_spacing=opts['cat_label_spacing'],
                                angle=opts['cat_label_angle'],
                                halign=opts['cat_label_halign'],
                                valign=opts['cat_label_valign'],
                                font_size=opts['cat_label_size'])

def get_y_tickmarker(tools, opts):
    return tools.new_tickmarker('vertical',
                                marker_args=dict(before=opts['val_tick_size'],
                                                 after=0),
                                label_spacing=opts['val_label_spacing'],
                                angle=opts['val_label_angle'],
                                halign=opts['val_label_halign'],
                                valign=opts['val_label_valign'],
                                font_size=opts['val_label_size'])
                                
                                
    

def configure_chart_horizontal(tools, opts, chart,
                               cat_data, cat_scale,
                               val_data, val_scale,
                               bar_origin,
                               generic_plot_params):
    p = tools.new_plot('hbar',
                       x_data=val_data,
                       y_data=cat_data,
                       x_scale=val_scale,
                       y_scale=cat_scale,
                       x_origin=bar_origin,
                       **generic_plot_params)
    chart.add_plot(p, 'main')
    x_tickmarker = get_x_tickmarker(tools, opts)
    y_tickmarker = get_y_tickmarker(tools, opts)
    cat_border = get_category_tick_border(tools, opts,
                                          cat_scale, "vnice",
                                          opts['left_border_line_style'],
                                          y_tickmarker)
    val_border = get_value_tick_border(tools, opts, val_data,
                                       val_scale, "htick",
                                       opts['bottom_border_line_style'],
                                       x_tickmarker)
    if cat_border is not None:
        chart.add_left_border(cat_border, width=.1)
    if val_border is not None:
        chart.add_bottom_border(val_border, height=.1)
        
    return val_border, cat_border


def configure_chart_vertical(tools, opts, chart,
                             cat_data, cat_scale,
                             val_data, val_scale,
                             bar_origin,
                             generic_plot_params):
    p = tools.new_plot('vbar',
                       x_data=cat_data,
                       y_data=val_data,
                       y_scale=val_scale,
                       x_scale=cat_scale,
                       y_origin=bar_origin,
                       **generic_plot_params)
    chart.add_plot(p, 'main')
    x_tickmarker = get_x_tickmarker(tools, opts)
    y_tickmarker = get_y_tickmarker(tools, opts)
    cat_border = get_category_tick_border(tools, opts,
                                          cat_scale, "hnice",
                                          opts['bottom_border_line_style'],
                                          x_tickmarker)
    val_border = get_value_tick_border(tools, opts, val_data,
                                       val_scale, "vtick",
                                       opts['left_border_line_style'],
                                       y_tickmarker)
    if cat_border is not None:
        chart.add_bottom_border(cat_border, height=.1)
    if val_border is not None:
        chart.add_left_border(val_border, width=.1)
        
    return cat_border, val_border



def OLDpad_chart(chart, opts):
    pad_opts = dict()
    if opts['padding']    is not None: pad_opts['allpad']  = opts['padding']
    if opts['pad_top']    is not None: pad_opts['tpad']    = opts['pad_top']
    if opts['pad_bottom'] is not None: pad_opts['bpad']    = opts['pad_bottom']
    if opts['pad_left']   is not None: pad_opts['lpad']    = opts['pad_left']
    if opts['pad_top']    is not None: pad_opts['rpad']    = opts['pad_right']
    # Are any of the values set? Then use the user-supplied opts
    if len(pad_opts) == 0:
        chart.set_padding(allpad="20px")
    else:
        chart.set_padding(**pad_opts)
    return

def pad_chart(chart, opts):
    def spec_from_opt(src):
        if opts[src] is not None:
            return "%dpx" % opts[src]
        else:
            return None

    pad_opts = dict()
    for argname, optname in (('allpad','padding'),
                             ('tpad', 'pad_top'), ('bpad', 'pad_bottom'),
                             ('lpad', 'pad_left'), ('rpad', 'pad_right')):
        if opts[optname] is not None:
            pad_opts[argname] = "%dpx" % opts[optname]
    
    
    # Are any of the values set? Then use the user-supplied opts
    if len(pad_opts) == 0:
        chart.set_padding(allpad="20px")
    else:
        chart.set_padding(**pad_opts)
    return


def grid_chart(tools, chart, opts, x_border, y_border):
    # x_border and y_border should be TicksetBorders (or None). By
    # convention, the first tickset added should be the one that
    # drives the gridlines.

    # TODO: this is pretty primitive
    plot = chart.get_plot('main')
    if opts['vgrid']:
        if x_border is not None and x_border.num_ticksets() > 0:
            x_scale = plot.get_scale('x')
            try:
                x_scale = x_scale.to_categorical_scale(alignment="center")
            except:
                # Must not be a CategoricalRangeScale
                pass
            vpos = tuple(x_border.iter_tick_positions(0))
            vgridlines = tools.new_gridlines(
                'vertical',
                pos_data=vpos,
                pos_scale=x_scale,
                line_style_scale=opts["vgrid_style"],
                line_width_scale=opts["vgrid_width"],
                line_color_scale=opts["vgrid_color"])
            chart.add_rear_gridlines(vgridlines)
    if opts['hgrid']:
        if y_border is not None and y_border.num_ticksets() > 0:
            y_scale = plot.get_scale('y')
            try:
                y_scale = y_scale.to_categorical_scale(alignment="center")
            except:
                # Must not be a CategoricalRangeScale
                pass
            hpos = tuple(y_border.iter_tick_positions(0))
            hgridlines = tools.new_gridlines(
                'horizontal',
                pos_data=hpos,
                pos_scale=y_scale,
                line_style_scale=opts['hgrid_style'],
                line_width_scale=opts['hgrid_width'],
                line_color_scale=opts['hgrid_color'])
            chart.add_rear_gridlines(hgridlines)


def title_chart(tools, chart, opts):
    if opts['title'] != "":
        chart.add_top_title(
            tools.new_border('hlabel', label=opts['title'],
                             font_size='large'),
            height=.1)
    if opts['caption'] != "":
        chart.add_bottom_title(
            tools.new_border('hlabel', label=opts['caption']),
            height=.1)
    if opts['bottom_label'] != "":
        chart.add_bottom_border(
            tools.new_border('hlabel', label=opts['bottom_label']),
            height=.1)
    if opts['left_label'] != "":
        chart.add_left_title(
            tools.new_border('vlabel', label=opts['left_label']),
            width=.1)


def main(opts):
    tools = toolbox.Toolbox.for_file()
    d = tools.new_dataset_from_stream(
        istream_from_param(opts['input_path']),
        first_line_is_colnames=opts['first_line_colnames'])
    c = tools.new_chart('square')
    c.set_chart_background(opts['chart_bgcolor'])
    c.set_plot_background(opts['plot_bgcolor'])

    cat_data = d.column(opts['cat_input'])
    val_data = d.column(opts['val_input'])

    if len(cat_data) == 0:
        debug("category data is empty!")
    else:
        debug("Type of category data is %s (e.g., '%s')" % (
            type(cat_data[0]),
            cat_data[0]))

    if len(val_data) == 0:
        debug("value data is empty!")
    else:
        debug("Type of value data is %s (e.g., '%s')" % (
            type(val_data[0]),
            val_data[0]))



    cat_scale = tools.new_scale('catrange', reverse=opts['reverse_cats'])
    
    if opts['val_scale'] == "log":
        val_scale = get_log_scale(tools, opts)
    else:
        val_scale = get_scale(tools, opts)


    generic_plot_params = dict(
        bar_color_scale=opts['bar_fill_color'],
        border_color_scale=opts['bar_border_color'],
        bar_width_scale=opts['bar_width'],
        border_width_scale=opts['bar_border_width'])
                                        
    if supplied(opts['bar_origin']):
        bar_origin = bar_origin
    else:
        # Default to 0 (or 1 for log scales)
        if opts['val_scale'] == 'log':
            bar_origin = 1
        else:
            bar_origin = 0


    if opts['horizontal']:
        x_border, y_border = configure_chart_horizontal(
                tools, opts, c,
                cat_data, cat_scale,
                val_data, val_scale,
                bar_origin,
                generic_plot_params)
        cat_border = y_border
        val_border = x_border
    else:
        x_border, y_border = configure_chart_vertical(
                tools, opts, c,
                cat_data, cat_scale,
                val_data, val_scale,
                bar_origin,
                generic_plot_params)
        cat_border = x_border
        val_border = y_border
                                       

    pad_chart(c, opts)

    grid_chart(tools, c, opts, x_border, y_border)

    title_chart(tools, c, opts)
    
    page = tools.new_page_from_filename(
        opts['output_path'], opts['width'], opts['height'])
    page.write(c)
    return 0



def cstr(spec): return miscutils.parse_color_string(spec)

border_choices = ['none', 'line']

border_line_style_choices = ['solid', 'dotted', 'dotdash', 'dashed', 'none']

def str_from_choices(choices):
    return ", ".join(["'%s'" % c for c in choices])


app_options = [
    Flag("horizontal",
         hlp="Display value on x axis, i.e. 'horizontal' bars. "
         "(Default: vertical bars)"),
    ColnameOption(
        "cat-input", default_val=0,
        hlp="Name or index (from left, starting at 0) of "
        "column containing category information (Default: 0)"),
    Flag("no-cat-ticks",
         hlp="Don't display category tickmarks (Default: display ticks"),
    Flag("reverse-cats",
         hlp="Reverse display order of categories. (Default: don't reverse)"),
    ColnameOption(
        "val-input", default_val=1,
        hlp="Name or index (from left, starting at 0) of "
        "column containing value information (Default: 1)"),
    StringOption(
        "val-scale", default_val="linear",
        hlp="Scale type to use for values. (Default: 'linear')"),
    FloatOption(
        "val-scale-min", default_val=None,
        hlp="Lower bound of value scale. "
        "(Default: 1 for log scale; 0 for everything else)"),
    NewTickspecOption(
        "val-ticks", default_val=parse_tickspec("val-ticks",
                                                "smin,smax"),
        hlp="positions of tickmarks. (Default: 'smin,smax')"),
    ColorOption(
        "bar-fill-color", default_val=cstr("#0000ffff"),
        hlp="Color of bars in barplots (Default: blue)"),
    ColorOption(
        "bar-border-color", default_val=cstr("#000000"),
        hlp="Color of bar borders in barplots (Default: black)"),
    FloatOption(
        "bar-width", default_val=.8,
        hlp="Bar width in barplots, as proportion of "
        "available (1 == 100%) (Default: .8)"),
    IntOption(
        "bar-border-width", default_val=1,
        hlp="Width (in pixels) of bar border (Default: 2)"),
    # TODO: implement this in the Plot classes
    # ChoiceOption(
    #     "bar-border-line-style", default_val="solid",
    #     choices = border_line_style_choices,
    #     hlp="Type of line to draw on around bars in plot. "
    #     "(Values: 'solid', 'dotted', 'dotdash', 'dashed', 'none')"
    #     "Default: 'solid'"),
    FloatOption(
        "bar-origin", default_val=NO_VALUE,
        hlp="Point on value scale from which bars "
        "should originate (Default: 0)"),
    ColorOption(
        "plot-bgcolor", default_val=cstr("#00000000"),
        hlp="Background color of plotting area (Default: transparent)"),
    ColorOption(
        "chart-bgcolor", default_val=cstr("#ffffffff"),
        hlp="Background color of chart area (Default: white)"),
    Flag("vgrid",
         hlp="Draw vertical grid lines? (Default: yes)"),
    StringOption(
        "vgrid-style", default_val="solid",
        hlp="Line style of vertical grid lines. (Default 'solid')"),
    IntOption(
        "vgrid-width", default_val=2,
        hlp="Line width of vertical grid lines, in pts/pixels. (Default: 2)"),
    ColorOption(
        "vgrid-color", default_val=cstr("#ccccccff"),
        hlp="Line color of vertical grid lines. (Default: gray)"),
    Flag("hgrid",
         hlp="Draw horizontal grid lines? (Default: yes)"),
    StringOption(
        "hgrid-style", default_val="solid",
        hlp="Line style of horizontal grid lines. (Default 'solid')"),
    IntOption(
        "hgrid-width", default_val=2,
        hlp="Line width of horizontal grid lines, in pts/pixels. (Default: 2)"),
    ColorOption(
        "hgrid-color", default_val=cstr("#ccccccff"),
        hlp="Line color of horizontal grid lines. (Default: gray)"),
    # Borders
    ChoiceOption(
        "bottom-border-line-style", default_val="solid",
        choices=border_line_style_choices,
        hlp="Type of line to draw on bottom edge of plot. "
        "(Values: 'solid', 'dotted', 'dotdash', 'dashed', 'none')"
        "Default: 'solid'"),
    ChoiceOption(
        "left-border-line-style", default_val="solid",
        choices=border_line_style_choices,
        hlp="Type of line to draw on left edge of plot. "
        "(Values: 'solid', 'dotted', 'dotdash', 'dashed', 'none')"
        "Default: 'solid'"),
    # Words around the vis
    # -- Bottom/Category label
    StringOption("bottom-label", default_val="", hlp="Bottom border label"),
    IntOption(
        "cat-label-angle", default_val=45,
        hlp="Degrees of rotation (0 == horizontal, left-to-right) "
        "to apply to text on X axis. (Default: 45)"),
    StringOption(
        "cat-label-halign", default_val="right",
        hlp="Horizontal alignment of text on X axis rel. to tick mark. "
        "(Values: 'left', 'center', 'right') (Default: 'right')"),
    StringOption(
        "cat-label-valign", default_val="center",
        hlp="Vertical alignment of text on X axis rel. to tick mark. "
        "(Values: 'top', 'center', 'bottom') (Default: 'center')"),
    StringOption(
        "cat-label-size", default_val="normal",
        hlp="Relative font size of X axis label. "
        "(Values: 'small', 'normal', 'large', 'x-large', 'x-large') "
        "(Default: 'normal')"),
    IntOption(
        "cat-label-spacing", default_val=10,
        hlp="Spacing between top of text and plot. (Default: 10)"),
    IntOption(
        "cat-tick-size", default_val=3,
        hlp="Size in pts/pixels of tick mark on category axis. (Default: 8)"),
    # -- Left/Value label
    StringOption("left-label", default_val="", hlp="Left  border label"),
    ChoiceOption(
        "val-label-size", default_val="normal",
        choices=('small', 'normal', 'large', 'x-large'),
        hlp="Relative font size of Y axis label. "
        "(Values: 'small', 'normal', 'large', 'x-large') "
        "(Default: 'normal')"),
    IntOption(
        "val-label-angle", default_val=0,
        hlp="Degrees of rotation (0 == horizontal, left-to-right) "
        "to apply to text on X axis. (Default: 0)"),
    StringOption(
        "val-label-halign", default_val="right",
        hlp="Horizontal alignment of text on Y axis rel. to tick mark. "
        "(Values: 'left', 'center', 'right') (Default: 'right')"),
    StringOption(
        "val-label-valign", default_val="center",
        hlp="Vertical alignment of text on Y axis rel. to tick mark. "
        "(Values: 'top', 'center', 'bottom') (Default: 'center')"),
    IntOption(
        "val-label-spacing", default_val=10,
        hlp="Spacing between left of text and plot. (Default: 10)"),
    IntOption(
        "val-tick-size", default_val=3,
        hlp="Size in pts/pixels of tick mark on y axis. (Default: 8)"),
    # -- Other
    StringOption("title", default_val="", hlp="Chart title"),
    StringOption("caption", default_val="", hlp="Chart caption"),
    # (End words-around-vis)
    IntOption(
        "padding", default_val=None,
        hlp="Padding to add to all sides of output. (Default: see man page)"),
    IntOption(
        "pad-top", default_val=None,
        hlp="Padding to add to top of output. (Default: see man page)"),
    IntOption(
        "pad-bottom", default_val=None,
        hlp="Padding to add to bottom of output. (Default: see man page)"),
    IntOption(
        "pad-left", default_val=None,
        hlp="Padding to add to left of output. (Default: see man page)"),
    IntOption(
        "pad-right", default_val=None,
        hlp="Padding to add to right of output. (Default: see man page)"),
]


desc = "plot categorical data"

execute(cmd_func=main, cmd_options=app_options, cmd_desc=desc)
